[EME] Batch multiple key requests into one request and response
authorjer.noble@apple.com <jer.noble@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 31 Oct 2019 23:54:38 +0000 (23:54 +0000)
committerjer.noble@apple.com <jer.noble@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 31 Oct 2019 23:54:38 +0000 (23:54 +0000)
https://bugs.webkit.org/show_bug.cgi?id=203580
<rdar://problem/54853345>

Reviewed by Eric Carlson.

Source/WebCore:

Test: platform/mac/media/encrypted-media/fps-multiple-pssh.html

Add a path for -didProvideRequests: giving an NSArray of AVContentKeyRequests to result in
an EME message containing a JSON object containing keyIDs and licence request payloads.

- CDMInstanceSessionFairPlayStreamingAVFObjC now defines a "Request" as a Vector of
  AVContentKeyRequests.
- When we receive an NSArray of AVContentKeyRequests, we store that as a single "Request"
  in m_currentRequest and m_requests, or in m_pendingRequests.
- Generating a key request message from such a vector will result in a JSON object in the format
  of [{ keyID: ..., payload: ...}, ...].
- Clients can update these all at once with a matching JSON object in the same format, or alternatively
  of the format [{ keyID: ..., error: ...}, ...] to indicate to the CDN that there was an error
  encountered while completing the key request.
- There are a couple of helper classes that facilitate collecting delegate callbacks and completion handlers
  into a single final callback: UpdateResponseCollector and CallbackAggregator.

* platform/graphics/avfoundation/objc/CDMInstanceFairPlayStreamingAVFObjC.h:
* platform/graphics/avfoundation/objc/CDMInstanceFairPlayStreamingAVFObjC.mm:
(-[WebCoreFPSContentKeySessionDelegate contentKeySession:didProvideContentKeyRequests:forInitializationData:]):
(WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::UpdateResponseCollector::UpdateResponseCollector):
(WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::UpdateResponseCollector::addSuccessfulResponse):
(WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::UpdateResponseCollector::addErrorResponse):
(WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::UpdateResponseCollector::fail):
(WebCore::parseJSONValue):
(WebCore::CDMInstanceFairPlayStreamingAVFObjC::sessionForKeyIDs const):
(WebCore::keyIDsForRequest):
(WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::keyIDs):
(WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::updateLicense):
(WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::closeSession):
(WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::didProvideRequest):
(WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::didProvideRequests):
(WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::didProvideRenewingRequest):
(WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::didFailToProvideRequest):
(WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::requestDidSucceed):
(WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::nextRequest):
(WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::lastKeyRequest const):
(WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::keyStatuses const):

Source/WTF:

Support appending an r-value reference Vector to another.

* wtf/Vector.h:
(WTF::minCapacity>::appendVector):

LayoutTests:

* platform/mac/TestExpectations:
* platform/mac/media/encrypted-media/fps-multiple-pssh-expected.txt: Added.
* platform/mac/media/encrypted-media/fps-multiple-pssh.html: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@251895 268f45cc-cd09-0410-ab3c-d52691b4dbfc

LayoutTests/ChangeLog
LayoutTests/platform/mac/TestExpectations
LayoutTests/platform/mac/media/encrypted-media/fps-multiple-pssh-expected.txt [new file with mode: 0644]
LayoutTests/platform/mac/media/encrypted-media/fps-multiple-pssh.html [new file with mode: 0644]
Source/WTF/ChangeLog
Source/WTF/wtf/Vector.h
Source/WebCore/ChangeLog
Source/WebCore/platform/graphics/avfoundation/objc/CDMInstanceFairPlayStreamingAVFObjC.h
Source/WebCore/platform/graphics/avfoundation/objc/CDMInstanceFairPlayStreamingAVFObjC.mm

index d9181a8..0b74042 100644 (file)
@@ -1,3 +1,15 @@
+2019-10-31  Jer Noble  <jer.noble@apple.com>
+
+        [EME] Batch multiple key requests into one request and response
+        https://bugs.webkit.org/show_bug.cgi?id=203580
+        <rdar://problem/54853345>
+
+        Reviewed by Eric Carlson.
+
+        * platform/mac/TestExpectations:
+        * platform/mac/media/encrypted-media/fps-multiple-pssh-expected.txt: Added.
+        * platform/mac/media/encrypted-media/fps-multiple-pssh.html: Added.
+
 2019-10-31  Nikita Vasilyev  <nvasilyev@apple.com>
 
         Web Inspector: Color picker: incorrect saturation when selecting color on color square
index 7d3ec48..b990e92 100644 (file)
@@ -2022,4 +2022,7 @@ webkit.org/b/203171 inspector/layers/layers-for-node.html [ Pass Failure ]
 webkit.org/b/203305 [ HighSierra Debug ] imported/w3c/web-platform-tests/css/css-transitions/properties-value-001.html [ Pass Failure ]
 webkit.org/b/203305 [ HighSierra Debug ] imported/w3c/web-platform-tests/css/css-transitions/properties-value-inherit-001.html [ Pass Failure ]
 webkit.org/b/203357 [ Debug ] imported/w3c/web-platform-tests/css/css-transitions/event-dispatch.tentative.html [ Pass Failure ]
-webkit.org/b/203356 [ Debug ] imported/w3c/web-platform-tests/css/css-transitions/properties-value-003.html [ Pass Failure ]
\ No newline at end of file
+webkit.org/b/203356 [ Debug ] imported/w3c/web-platform-tests/css/css-transitions/properties-value-003.html [ Pass Failure ]
+
+# rdar://54275897 ([ macOS ] Add support for batched key requests)
+platform/mac/media/encrypted-media/fps-multiple-pssh.html [ Skip ]
diff --git a/LayoutTests/platform/mac/media/encrypted-media/fps-multiple-pssh-expected.txt b/LayoutTests/platform/mac/media/encrypted-media/fps-multiple-pssh-expected.txt
new file mode 100644 (file)
index 0000000..09865ce
--- /dev/null
@@ -0,0 +1,11 @@
+RUN(promise = navigator.requestMediaKeySystemAccess("com.apple.fps", capabilities))
+Promise resolved OK
+RUN(promise = access.createMediaKeys())
+Promise resolved OK
+RUN(keys.setServerCertificate(serverCertificate))
+RUN(session = keys.createSession())
+EXPECTED (session != 'null') OK
+RUN(promise = session.generateRequest("cenc", initData.buffer))
+Promise resolved OK
+END OF TEST
+
diff --git a/LayoutTests/platform/mac/media/encrypted-media/fps-multiple-pssh.html b/LayoutTests/platform/mac/media/encrypted-media/fps-multiple-pssh.html
new file mode 100644 (file)
index 0000000..4df7db2
--- /dev/null
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<head>
+       <title>fps-muliple-pssh.html</title>
+    <script src="../../../../media/video-test.js"></script>
+       <script>
+    var capabilities = [{
+        initDataTypes: ['cenc'],
+        videoCapabilities: [{ contentType: 'video/mp4', robustness: '' }],
+        distinctiveIdentifier: 'not-allowed',
+        persistentState: 'not-allowed',
+        sessionTypes: ['temporary'],
+    }];
+    var promise;
+    var access;
+    var keys;
+    var session;
+    var initData = Uint8Array.of(0x00, 0x00, 0x00, 0x80, 0x70, 0x73, 0x73, 0x68, 0x00, 0x00, 0x00, 0x00, 0x94, 0xCE, 0x86, 0xFB, 0x07, 0xFF, 0x4F, 0x43, 0xAD, 0xB8, 0x93, 0xD2, 0xFA, 0x96, 0x8C, 0xA2, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x60, 0x66, 0x70, 0x73, 0x64, 0x00, 0x00, 0x00, 0x10, 0x66, 0x70, 0x73, 0x69, 0x00, 0x00, 0x00, 0x00, 0x63, 0x65, 0x6E, 0x63, 0x00, 0x00, 0x00, 0x24, 0x66, 0x70, 0x73, 0x6B, 0x00, 0x00, 0x00, 0x1C, 0x66, 0x6B, 0x72, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x24, 0x66, 0x70, 0x73, 0x6B, 0x00, 0x00, 0x00, 0x1C, 0x66, 0x6B, 0x72, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02);
+    var serverCertificateURI = "data:application/x-x509-ca-cert;base64,MIIEyDCCA7CgAwIBAgIID0/07cCHH0YwDQYJKoZIhvcNAQEFBQAwfzELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkFwcGxlIEluYy4xJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MTMwMQYDVQQDDCpBcHBsZSBLZXkgU2VydmljZXMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTIxMjIwMDAzODQzWhcNMTQxMjIxMDAzODQzWjBIMQswCQYDVQQGEwJVUzESMBAGA1UECgwJQXBwbGUgSW5jMRIwEAYDVQQLDAlBcHBsZSBJbmMxETAPBgNVBAMMCGZwc3Rlc3Q0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCuHevDlphOM0sD67r3olFkN/2vC0oPl+dJ2CrBkL0tUJov9YbXud6ymJp2TkKkGqnubRaX5mI94+V9Cc/0zSlp+NTCDTcZ7y44E8j85Av/5XqozUf/wUyY+UYPBRD6BHUnH5YD6uuSlLcqE0DaE8ptXiQyN3SRteCFQ4nI9f11uQIDAQABo4ICATCCAf0wHQYDVR0OBBYEFBaVf1g1bfyAW776Sfveqj9N03EMMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUY+RHVMuFcVlGLIOszEQxZGcDLL4wgeIGA1UdIASB2jCB1zCB1AYJKoZIhvdjZAUBMIHGMIHDBggrBgEFBQcCAjCBtgyBs1JlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMDUGA1UdHwQuMCwwKqAooCaGJGh0dHA6Ly9jcmwuYXBwbGUuY29tL2tleXNlcnZpY2VzLmNybDAOBgNVHQ8BAf8EBAMCBSAwNAYLKoZIhvdjZAYNAQMBAf8EIgHGeq0WCxH3oALqbKRDeCl4lqDaHVSQjsQZnyCME2TecUEwSwYLKoZIhvdjZAYNAQQBAf8EOQHlHhdsgB8QPJZFb9aAmvc8kYSTQYbjF6/U2mq46sXWRKWPKGpqLyC5ActNQUuHjynZn6Y0vs0D8jANBgkqhkiG9w0BAQUFAAOCAQEAg8GkEw0gDRn7raA8w+V36NOvKlUx3Wh3pcqI8cRATv9Twp4zfyJ4FwxdT90/zCtUUjTVtau6yESTwX+LUFu/Y0kvaV0htgBIBu7MWYCvfLlSwS4MqbBtNdloDNvU7CNyWXaMpKpYKN6i0SqEnTvF0mSTpBliCT+QxuNIxaWq9h2cCn79kbXJM5+IC37mIWO0jvzQjeSlOXJPZrNHZ6Bjt0AyiCIiZVkPmVm5lV3Ycd0gf4mfhAVJYE/p6/dTk+mqnxDdUUMVFDUfu1OqBim6ldWnAh8PlWaGh5rLYVgGvxEEPDNeueYhxTrDnENbhCZ5n/by0Rp0s67t1924Wk+QUQ==";
+    var serverCertificate;
+
+    window.addEventListener('load', async event => {
+       try {
+               run('promise = navigator.requestMediaKeySystemAccess("com.apple.fps", capabilities)');
+               window.access = await shouldResolve(promise);
+
+               run('promise = access.createMediaKeys()');
+               window.keys = await shouldResolve(promise);
+
+               var response = await fetch(serverCertificateURI);
+            serverCertificate = await response.arrayBuffer();
+
+               run('keys.setServerCertificate(serverCertificate)');
+               run('session = keys.createSession()');
+               testExpected('session', 'null', '!=');
+               run('promise = session.generateRequest("cenc", initData.buffer)');
+               await shouldResolve(promise);
+           } finally {
+               endTest();
+           }
+    });
+
+       </script>
+</head>
+<body>
+
+</body>
+</html>
\ No newline at end of file
index a990e64..27a46ea 100644 (file)
@@ -1,3 +1,16 @@
+2019-10-31  Jer Noble  <jer.noble@apple.com>
+
+        [EME] Batch multiple key requests into one request and response
+        https://bugs.webkit.org/show_bug.cgi?id=203580
+        <rdar://problem/54853345>
+
+        Reviewed by Eric Carlson.
+
+        Support appending an r-value reference Vector to another.
+
+        * wtf/Vector.h:
+        (WTF::minCapacity>::appendVector):
+
 2019-10-31  Alex Christensen  <achristensen@webkit.org>
 
         Remove unneeded HAVE_TIMINGDATAOPTIONS
index 051396d..b7cde78 100644 (file)
@@ -776,6 +776,7 @@ public:
 
     template<typename U> void append(const U*, size_t);
     template<typename U, size_t otherCapacity> void appendVector(const Vector<U, otherCapacity>&);
+    template<typename U, size_t otherCapacity> void appendVector(Vector<U, otherCapacity>&&);
     template<typename U> bool tryAppend(const U*, size_t);
 
     template<typename U> void insert(size_t position, const U*, size_t);
@@ -1409,6 +1410,17 @@ inline void Vector<T, inlineCapacity, OverflowHandler, minCapacity>::appendVecto
 }
 
 template<typename T, size_t inlineCapacity, typename OverflowHandler, size_t minCapacity>
+template<typename U, size_t otherCapacity>
+inline void Vector<T, inlineCapacity, OverflowHandler, minCapacity>::appendVector(Vector<U, otherCapacity>&& val)
+{
+    size_t newSize = m_size + val.size();
+    if (newSize > capacity())
+        expandCapacity(newSize);
+    for (auto& item : val)
+        uncheckedAppend(WTFMove(item));
+}
+
+template<typename T, size_t inlineCapacity, typename OverflowHandler, size_t minCapacity>
 template<typename U>
 void Vector<T, inlineCapacity, OverflowHandler, minCapacity>::insert(size_t position, const U* data, size_t dataSize)
 {
index 90bd214..e804a7e 100644 (file)
@@ -1,3 +1,50 @@
+2019-10-31  Jer Noble  <jer.noble@apple.com>
+
+        [EME] Batch multiple key requests into one request and response
+        https://bugs.webkit.org/show_bug.cgi?id=203580
+        <rdar://problem/54853345>
+
+        Reviewed by Eric Carlson.
+
+        Test: platform/mac/media/encrypted-media/fps-multiple-pssh.html
+
+        Add a path for -didProvideRequests: giving an NSArray of AVContentKeyRequests to result in
+        an EME message containing a JSON object containing keyIDs and licence request payloads.
+
+        - CDMInstanceSessionFairPlayStreamingAVFObjC now defines a "Request" as a Vector of
+          AVContentKeyRequests.
+        - When we receive an NSArray of AVContentKeyRequests, we store that as a single "Request"
+          in m_currentRequest and m_requests, or in m_pendingRequests.
+        - Generating a key request message from such a vector will result in a JSON object in the format
+          of [{ keyID: ..., payload: ...}, ...].
+        - Clients can update these all at once with a matching JSON object in the same format, or alternatively
+          of the format [{ keyID: ..., error: ...}, ...] to indicate to the CDN that there was an error
+          encountered while completing the key request.
+        - There are a couple of helper classes that facilitate collecting delegate callbacks and completion handlers
+          into a single final callback: UpdateResponseCollector and CallbackAggregator.
+
+        * platform/graphics/avfoundation/objc/CDMInstanceFairPlayStreamingAVFObjC.h:
+        * platform/graphics/avfoundation/objc/CDMInstanceFairPlayStreamingAVFObjC.mm:
+        (-[WebCoreFPSContentKeySessionDelegate contentKeySession:didProvideContentKeyRequests:forInitializationData:]):
+        (WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::UpdateResponseCollector::UpdateResponseCollector):
+        (WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::UpdateResponseCollector::addSuccessfulResponse):
+        (WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::UpdateResponseCollector::addErrorResponse):
+        (WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::UpdateResponseCollector::fail):
+        (WebCore::parseJSONValue):
+        (WebCore::CDMInstanceFairPlayStreamingAVFObjC::sessionForKeyIDs const):
+        (WebCore::keyIDsForRequest):
+        (WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::keyIDs):
+        (WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::updateLicense):
+        (WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::closeSession):
+        (WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::didProvideRequest):
+        (WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::didProvideRequests):
+        (WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::didProvideRenewingRequest):
+        (WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::didFailToProvideRequest):
+        (WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::requestDidSucceed):
+        (WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::nextRequest):
+        (WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::lastKeyRequest const):
+        (WebCore::CDMInstanceSessionFairPlayStreamingAVFObjC::keyStatuses const):
+
 2019-10-31  Alex Christensen  <achristensen@apple.com>
 
         Enable more features in Mac CMake build
index 0f4370a..6a6bd1f 100644 (file)
@@ -72,7 +72,9 @@ public:
     SharedBuffer* serverCertificate() const { return m_serverCertificate.get(); }
 
     void outputObscuredDueToInsufficientExternalProtectionChanged(bool);
-    CDMInstanceSessionFairPlayStreamingAVFObjC* sessionForKeyIDs(const Vector<Ref<SharedBuffer>>&) const;
+
+    using Keys = Vector<Ref<SharedBuffer>>;
+    CDMInstanceSessionFairPlayStreamingAVFObjC* sessionForKeyIDs(const Keys&) const;
 
 private:
     RefPtr<SharedBuffer> m_serverCertificate;
@@ -97,6 +99,7 @@ public:
     void clearClient() final;
 
     void didProvideRequest(AVContentKeyRequest*);
+    void didProvideRequests(Vector<RetainPtr<AVContentKeyRequest>>&&);
     void didProvideRenewingRequest(AVContentKeyRequest*);
     void didProvidePersistableRequest(AVContentKeyRequest*);
     void didFailToProvideRequest(AVContentKeyRequest*, NSError*);
@@ -105,27 +108,34 @@ public:
     void sessionIdentifierChanged(NSData*);
     void outputObscuredDueToInsufficientExternalProtectionChanged(bool);
 
-    Vector<Ref<SharedBuffer>> keyIDs();
+    using Keys = CDMInstanceFairPlayStreamingAVFObjC::Keys;
+    Keys keyIDs();
     AVContentKeySession* contentKeySession() { return m_session.get(); }
 
+    using Request = Vector<RetainPtr<AVContentKeyRequest>>;
+
 private:
     AVContentKeySession* ensureSession();
     bool isLicenseTypeSupported(LicenseType) const;
 
     KeyStatusVector keyStatuses() const;
     void nextRequest();
+    AVContentKeyRequest* lastKeyRequest() const;
 
     Ref<CDMInstanceFairPlayStreamingAVFObjC> m_instance;
     RetainPtr<AVContentKeySession> m_session;
-    RetainPtr<AVContentKeyRequest> m_currentRequest;
+    Request m_currentRequest;
     RetainPtr<WebCoreFPSContentKeySessionDelegate> m_delegate;
     Vector<RetainPtr<NSData>> m_expiredSessions;
     WeakPtr<CDMInstanceSessionClient> m_client;
     String m_sessionId;
     bool m_outputObscured { false };
 
-    Vector<RetainPtr<AVContentKeyRequest>> m_pendingRequests;
-    Vector<RetainPtr<AVContentKeyRequest>> m_requests;
+    class UpdateResponseCollector;
+    std::unique_ptr<UpdateResponseCollector> m_updateResponseCollector;
+
+    Vector<Request> m_pendingRequests;
+    Vector<Request> m_requests;
 
     LicenseCallback m_requestLicenseCallback;
     LicenseUpdateCallback m_updateLicenseCallback;
index dc7142b..0f77708 100644 (file)
@@ -40,6 +40,7 @@
 #import <pal/spi/mac/AVFoundationSPI.h>
 #import <wtf/Algorithms.h>
 #import <wtf/FileSystem.h>
+#import <wtf/JSONValues.h>
 #import <wtf/text/Base64.h>
 #import <wtf/text/StringHash.h>
 
@@ -98,6 +99,21 @@ static const NSString *PlaybackSessionIdKey = @"PlaybackSessionID";
 }
 #endif
 
+- (void)contentKeySession:(AVContentKeySession *)session didProvideContentKeyRequests:(NSArray<AVContentKeyRequest *> *)keyRequests forInitializationData:(nullable NSData *)initializationData
+{
+    UNUSED_PARAM(session);
+    UNUSED_PARAM(initializationData);
+    if (!_parent)
+        return;
+
+    Vector<RetainPtr<AVContentKeyRequest>> requests;
+    requests.reserveInitialCapacity(keyRequests.count);
+    [keyRequests enumerateObjectsUsingBlock:[&](AVContentKeyRequest* request, NSUInteger, BOOL*) {
+        requests.uncheckedAppend(request);
+    }];
+    _parent->didProvideRequests(WTFMove(requests));
+}
+
 - (void)contentKeySession:(AVContentKeySession *)session contentKeyRequest:(AVContentKeyRequest *)keyRequest didFailWithError:(NSError *)err
 {
     UNUSED_PARAM(session);
@@ -129,6 +145,59 @@ static const NSString *PlaybackSessionIdKey = @"PlaybackSessionID";
 
 namespace WebCore {
 
+class CDMInstanceSessionFairPlayStreamingAVFObjC::UpdateResponseCollector {
+    WTF_MAKE_FAST_ALLOCATED;
+public:
+    using KeyType = RetainPtr<AVContentKeyRequest>;
+    using ValueType = RetainPtr<NSData>;
+    using ResponseMap = HashMap<KeyType, ValueType>;
+    using UpdateCallback = WTF::Function<void(Optional<ResponseMap>&&)>;
+    UpdateResponseCollector(size_t numberOfExpectedResponses, UpdateCallback&& callback)
+        : m_numberOfExpectedResponses(numberOfExpectedResponses)
+        , m_callback(WTFMove(callback))
+    {
+    }
+
+    void addSuccessfulResponse(AVContentKeyRequest* request, NSData* data)
+    {
+        m_responses.set(request, data);
+        if (!--m_numberOfExpectedResponses)
+            m_callback(WTFMove(m_responses));
+    }
+    void addErrorResponse(AVContentKeyRequest* request, NSError* error)
+    {
+        UNUSED_PARAM(error);
+        m_responses.set(request, nullptr);
+        if (!--m_numberOfExpectedResponses)
+            m_callback(WTFMove(m_responses));
+    }
+    void fail()
+    {
+        m_callback(WTF::nullopt);
+    }
+
+private:
+    size_t m_numberOfExpectedResponses;
+    UpdateCallback m_callback;
+    ResponseMap m_responses;
+};
+
+static RefPtr<JSON::Value> parseJSONValue(const SharedBuffer& buffer)
+{
+    // Fail on large buffers whose size doesn't fit into a 32-bit unsigned integer.
+    size_t size = buffer.size();
+    if (size > std::numeric_limits<unsigned>::max())
+        return nullptr;
+
+    // Parse the buffer contents as JSON, returning the root object (if any).
+    String json { buffer.data(), static_cast<unsigned>(size) };
+    RefPtr<JSON::Value> value;
+    if (!JSON::Value::parseJSON(json, value))
+        return nullptr;
+
+    return value;
+}
+
 bool CDMInstanceFairPlayStreamingAVFObjC::supportsPersistableState()
 {
     return [PAL::getAVContentKeySessionClass() respondsToSelector:@selector(pendingExpiredSessionReportsWithAppIdentifier:storageDirectoryAtURL:)];
@@ -244,7 +313,7 @@ void CDMInstanceFairPlayStreamingAVFObjC::outputObscuredDueToInsufficientExterna
     }
 }
 
-CDMInstanceSessionFairPlayStreamingAVFObjC* CDMInstanceFairPlayStreamingAVFObjC::sessionForKeyIDs(const Vector<Ref<SharedBuffer>>& keyIDs) const
+CDMInstanceSessionFairPlayStreamingAVFObjC* CDMInstanceFairPlayStreamingAVFObjC::sessionForKeyIDs(const Keys& keyIDs) const
 {
     for (auto& sessionInterface : m_sessions) {
         if (!sessionInterface)
@@ -270,12 +339,13 @@ CDMInstanceSessionFairPlayStreamingAVFObjC::~CDMInstanceSessionFairPlayStreaming
     [m_delegate invalidate];
 }
 
-static Vector<Ref<SharedBuffer>> keyIDsForRequest(AVContentKeyRequest* request)
+using Keys = CDMInstanceSessionFairPlayStreamingAVFObjC::Keys;
+static Keys keyIDsForRequest(AVContentKeyRequest* request)
 {
     if ([request.identifier isKindOfClass:[NSString class]])
-        return Vector<Ref<SharedBuffer>>::from(SharedBuffer::create([(NSString *)request.identifier dataUsingEncoding:NSUTF8StringEncoding]));
+        return Keys::from(SharedBuffer::create([(NSString *)request.identifier dataUsingEncoding:NSUTF8StringEncoding]));
     if ([request.identifier isKindOfClass:[NSData class]])
-        return Vector<Ref<SharedBuffer>>::from(SharedBuffer::create((NSData *)request.identifier));
+        return Keys::from(SharedBuffer::create((NSData *)request.identifier));
     if (request.initializationData) {
         if (auto sinfKeyIDs = CDMPrivateFairPlayStreaming::extractKeyIDsSinf(SharedBuffer::create(request.initializationData)))
             return WTFMove(sinfKeyIDs.value());
@@ -283,13 +353,22 @@ static Vector<Ref<SharedBuffer>> keyIDsForRequest(AVContentKeyRequest* request)
     return { };
 }
 
-Vector<Ref<SharedBuffer>> CDMInstanceSessionFairPlayStreamingAVFObjC::keyIDs()
+using Request = CDMInstanceSessionFairPlayStreamingAVFObjC::Request;
+static Keys keyIDsForRequest(const Request& requests)
+{
+    Keys keyIDs;
+    for (auto& request : requests)
+        keyIDs.appendVector(keyIDsForRequest(request.get()));
+    return keyIDs;
+}
+
+Keys CDMInstanceSessionFairPlayStreamingAVFObjC::keyIDs()
 {
     // FIXME(rdar://problem/35597141): use the future AVContentKeyRequest keyID property, rather than parsing it out of the init
     // data, to get the keyID.
-    Vector<Ref<SharedBuffer>> keyIDs;
+    Keys keyIDs;
     for (auto& request : m_requests) {
-        for (auto& key : keyIDsForRequest(request.get()))
+        for (auto& key : keyIDsForRequest(request))
             keyIDs.append(WTFMove(key));
     }
 
@@ -375,22 +454,115 @@ void CDMInstanceSessionFairPlayStreamingAVFObjC::updateLicense(const String&, Li
     }
 
     if (!m_requests.isEmpty() && isEqual(responseData, "renew"_s)) {
-        [m_session renewExpiringResponseDataForContentKeyRequest:m_requests.last().get()];
+        auto request = lastKeyRequest();
+        if (!request) {
+            callback(false, WTF::nullopt, WTF::nullopt, WTF::nullopt, Failed);
+            return;
+        }
+        [m_session renewExpiringResponseDataForContentKeyRequest:request];
         m_updateLicenseCallback = WTFMove(callback);
         return;
     }
 
-    if (!m_currentRequest) {
+    if (m_currentRequest.isEmpty()) {
         callback(false, WTF::nullopt, WTF::nullopt, WTF::nullopt, Failed);
         return;
     }
-    Vector<Ref<SharedBuffer>> keyIDs = keyIDsForRequest(m_currentRequest.get());
+    Keys keyIDs = keyIDsForRequest(m_currentRequest);
     if (keyIDs.isEmpty()) {
         callback(false, WTF::nullopt, WTF::nullopt, WTF::nullopt, Failed);
         return;
     }
 
-    [m_currentRequest processContentKeyResponse:[PAL::getAVContentKeyResponseClass() contentKeyResponseWithFairPlayStreamingKeyResponseData:responseData.createNSData().get()]];
+    if (m_currentRequest.size() > 1) {
+        if (m_updateResponseCollector) {
+            m_updateResponseCollector->fail();
+            m_updateResponseCollector = nullptr;
+        }
+
+        m_updateResponseCollector = WTF::makeUnique<UpdateResponseCollector>(m_currentRequest.size(), [weakThis = makeWeakPtr(*this), this] (Optional<UpdateResponseCollector::ResponseMap>&& responses) {
+            if (!weakThis)
+                return;
+
+            if (!m_updateLicenseCallback)
+                return;
+
+            if (!responses || responses.value().isEmpty()) {
+                m_updateLicenseCallback(true, WTF::nullopt, WTF::nullopt, WTF::nullopt, Failed);
+                return;
+            }
+
+            m_updateLicenseCallback(false, keyStatuses(), WTF::nullopt, WTF::nullopt, Succeeded);
+            m_updateResponseCollector = nullptr;
+            m_currentRequest.clear();
+            nextRequest();
+        });
+
+        auto root = parseJSONValue(responseData);
+        RefPtr<JSON::Array> array;
+        if (!root || !root->asArray(array)) {
+            callback(false, WTF::nullopt, WTF::nullopt, WTF::nullopt, Failed);
+            return;
+        }
+
+        auto parseResponse = [&](RefPtr<JSON::Value>& value) -> bool {
+            RefPtr<JSON::Object> object;
+            if (!value->asObject(object))
+                return false;
+
+            String keyIDString;
+            if (!object->getString("keyID", keyIDString))
+                return false;
+
+            Vector<uint8_t> keyIDVector;
+            if (!base64Decode(keyIDString, keyIDVector))
+                return false;
+
+            auto keyID = SharedBuffer::create(WTFMove(keyIDVector));
+            auto foundIndex = m_currentRequest.findMatching([&] (auto& request) {
+                auto keyIDs = keyIDsForRequest(request.get());
+                return keyIDs.contains(keyID);
+            });
+            if (foundIndex == notFound)
+                return false;
+
+            auto& request = m_currentRequest[foundIndex];
+
+            auto payloadFindResults = object->find("payload");
+            auto errorFindResults = object->find("error");
+            bool hasPayload = payloadFindResults != object->end();
+            bool hasError = errorFindResults != object->end();
+
+            // Either "payload" or "error" are present, but not both
+            if (hasPayload == hasError)
+                return false;
+
+            if (hasError) {
+                NSInteger errorCode;
+                if (!errorFindResults->value->asInteger(errorCode))
+                    return false;
+                auto error = adoptNS([[NSError alloc] initWithDomain:@"org.webkit.eme" code:errorCode userInfo:nil]);
+                [request processContentKeyResponseError:error.get()];
+            } else if (hasPayload) {
+                String payloadString;
+                if (!payloadFindResults->value->asString(payloadString))
+                    return false;
+                Vector<uint8_t> payloadVector;
+                if (!base64Decode(payloadString, payloadVector))
+                    return false;
+                auto payloadData = SharedBuffer::create(WTFMove(payloadVector));
+                [request processContentKeyResponse:[PAL::getAVContentKeyResponseClass() contentKeyResponseWithFairPlayStreamingKeyResponseData:payloadData->createNSData().get()]];
+            }
+            return true;
+        };
+        for (auto value : *array) {
+            if (!parseResponse(value)) {
+                callback(false, WTF::nullopt, WTF::nullopt, WTF::nullopt, Failed);
+                return;
+            }
+        }
+    } else
+        [m_currentRequest.first() processContentKeyResponse:[PAL::getAVContentKeyResponseClass() contentKeyResponseWithFairPlayStreamingKeyResponseData:responseData.createNSData().get()]];
 
     // FIXME(rdar://problem/35592277): stash the callback and call it once AVContentKeyResponse supports a success callback.
     struct objc_method_description method = protocol_getMethodDescription(@protocol(AVContentKeySessionDelegate), @selector(contentKeySession:contentKeyRequestDidSucceed:), NO, YES);
@@ -458,7 +630,7 @@ void CDMInstanceSessionFairPlayStreamingAVFObjC::closeSession(const String&, Clo
         m_removeSessionDataCallback({ }, WTF::nullopt, Failed);
         ASSERT(!m_removeSessionDataCallback);
     }
-    m_currentRequest = nullptr;
+    m_currentRequest.clear();
     m_pendingRequests.clear();
     m_requests.clear();
     callback();
@@ -518,14 +690,13 @@ void CDMInstanceSessionFairPlayStreamingAVFObjC::clearClient()
 
 void CDMInstanceSessionFairPlayStreamingAVFObjC::didProvideRequest(AVContentKeyRequest *request)
 {
-    if (m_currentRequest) {
-        m_pendingRequests.append(request);
+    if (!m_currentRequest.isEmpty()) {
+        m_pendingRequests.append({ retainPtr(request) });
         return;
     }
 
-    m_currentRequest = request;
+    m_currentRequest = { retainPtr(request) };
 
-    ASSERT(!m_requests.contains(m_currentRequest));
     m_requests.append(m_currentRequest);
 
     RetainPtr<NSData> appIdentifier;
@@ -542,7 +713,7 @@ void CDMInstanceSessionFairPlayStreamingAVFObjC::didProvideRequest(AVContentKeyR
     }
 
     RetainPtr<NSData> contentIdentifier = keyIDs.first()->createNSData();
-    [m_currentRequest makeStreamingContentKeyRequestDataForApp:appIdentifier.get() contentIdentifier:contentIdentifier.get() options:nil completionHandler:[this, weakThis = makeWeakPtr(*this)] (NSData *contentKeyRequestData, NSError *error) mutable {
+    [request makeStreamingContentKeyRequestDataForApp:appIdentifier.get() contentIdentifier:contentIdentifier.get() options:nil completionHandler:[this, weakThis = makeWeakPtr(*this)] (NSData *contentKeyRequestData, NSError *error) mutable {
         callOnMainThread([this, weakThis = WTFMove(weakThis), error = retainPtr(error), contentKeyRequestData = retainPtr(contentKeyRequestData)] {
             if (!weakThis)
                 return;
@@ -560,15 +731,95 @@ void CDMInstanceSessionFairPlayStreamingAVFObjC::didProvideRequest(AVContentKeyR
     }];
 }
 
+void CDMInstanceSessionFairPlayStreamingAVFObjC::didProvideRequests(Vector<RetainPtr<AVContentKeyRequest>>&& requests)
+{
+    if (!m_currentRequest.isEmpty()) {
+        m_pendingRequests.append(WTFMove(requests));
+        return;
+    }
+
+    m_currentRequest = requests;
+    m_requests.append(WTFMove(requests));
+
+    RetainPtr<NSData> appIdentifier;
+    if (auto* certificate = m_instance->serverCertificate())
+        appIdentifier = certificate->createNSData();
+
+    using RequestsData = Vector<std::pair<RefPtr<SharedBuffer>, RetainPtr<NSData>>>;
+    struct CallbackAggregator final : public ThreadSafeRefCounted<CallbackAggregator> {
+        using CallbackFunction = Function<void(RequestsData&&)>;
+        static RefPtr<CallbackAggregator> create(CallbackFunction&& completionHandler)
+        {
+            return adoptRef(new CallbackAggregator(WTFMove(completionHandler)));
+        }
+
+        explicit CallbackAggregator(Function<void(RequestsData&&)>&& completionHandler)
+            : m_completionHandler(WTFMove(completionHandler))
+        {
+        }
+
+        ~CallbackAggregator()
+        {
+            callOnMainThread([completionHandler = WTFMove(m_completionHandler), requestsData = WTFMove(requestsData)] () mutable {
+                completionHandler(WTFMove(requestsData));
+            });
+        }
+
+        CompletionHandler<void(RequestsData&&)> m_completionHandler;
+        RequestsData requestsData;
+    };
+
+    auto aggregator = CallbackAggregator::create([this, weakThis = makeWeakPtr(*this)] (RequestsData&& requestsData) {
+        if (!weakThis)
+            return;
+
+        if (!m_requestLicenseCallback)
+            return;
+
+        if (requestsData.isEmpty()) {
+            m_requestLicenseCallback(SharedBuffer::create(), m_sessionId, false, Failed);
+            return;
+        }
+
+        auto requestJSON = JSON::Array::create();
+        for (auto& requestData : requestsData) {
+            auto entry = JSON::Object::create();
+            auto& keyID = requestData.first;
+            auto& payload = requestData.second;
+            entry->setString("keyID", base64Encode(keyID->data(), keyID->size()));
+            entry->setString("payload", base64Encode(payload.get().bytes, payload.get().length));
+            requestJSON->pushObject(WTFMove(entry));
+        }
+        auto requestBuffer = utf8Buffer(requestJSON->toJSONString());
+        if (!requestBuffer) {
+            m_requestLicenseCallback(SharedBuffer::create(), m_sessionId, false, Failed);
+            return;
+        }
+        m_requestLicenseCallback(requestBuffer.releaseNonNull(), m_sessionId, false, Succeeded);
+    });
+
+    for (auto request : m_currentRequest) {
+        auto keyIDs = keyIDsForRequest(request.get());
+        RefPtr<SharedBuffer> keyID = WTFMove(keyIDs.first());
+        auto contentIdentifier = keyID->createNSData();
+        [request makeStreamingContentKeyRequestDataForApp:appIdentifier.get() contentIdentifier:contentIdentifier.get() options:nil completionHandler:[keyID = WTFMove(keyID), aggregator = aggregator.copyRef()] (NSData *contentKeyRequestData, NSError *error) mutable {
+            UNUSED_PARAM(error);
+            callOnMainThread([keyID = WTFMove(keyID), aggregator = WTFMove(aggregator), contentKeyRequestData = retainPtr(contentKeyRequestData)] () mutable {
+                aggregator->requestsData.append({ WTFMove(keyID), WTFMove(contentKeyRequestData) });
+            });
+        }];
+    }
+}
+
 void CDMInstanceSessionFairPlayStreamingAVFObjC::didProvideRenewingRequest(AVContentKeyRequest *request)
 {
     ASSERT(!m_requestLicenseCallback);
-    if (m_currentRequest) {
-        m_pendingRequests.append(request);
+    if (!m_currentRequest.isEmpty()) {
+        m_pendingRequests.append({ retainPtr(request) });
         return;
     }
 
-    m_currentRequest = request;
+    m_currentRequest = { retainPtr(request) };
 
     // The assumption here is that AVContentKeyRequest will only ever notify us of a renewing request as a result of calling
     // -renewExpiringResponseDataForContentKeyRequest: with an existing request.
@@ -577,10 +828,10 @@ void CDMInstanceSessionFairPlayStreamingAVFObjC::didProvideRenewingRequest(AVCon
     RetainPtr<NSData> appIdentifier;
     if (auto* certificate = m_instance->serverCertificate())
         appIdentifier = certificate->createNSData();
-    auto keyIDs = keyIDsForRequest(m_currentRequest.get());
+    auto keyIDs = keyIDsForRequest(m_currentRequest);
 
     RetainPtr<NSData> contentIdentifier = keyIDs.first()->createNSData();
-    [m_currentRequest makeStreamingContentKeyRequestDataForApp:appIdentifier.get() contentIdentifier:contentIdentifier.get() options:nil completionHandler:[this, weakThis = makeWeakPtr(*this)] (NSData *contentKeyRequestData, NSError *error) mutable {
+    [request makeStreamingContentKeyRequestDataForApp:appIdentifier.get() contentIdentifier:contentIdentifier.get() options:nil completionHandler:[this, weakThis = makeWeakPtr(*this)] (NSData *contentKeyRequestData, NSError *error) mutable {
         callOnMainThread([this, weakThis = WTFMove(weakThis), error = retainPtr(error), contentKeyRequestData = retainPtr(contentKeyRequestData)] {
             if (!weakThis || !m_client || error)
                 return;
@@ -605,12 +856,18 @@ void CDMInstanceSessionFairPlayStreamingAVFObjC::didFailToProvideRequest(AVConte
 {
     UNUSED_PARAM(request);
     UNUSED_PARAM(error);
+
+    if (m_updateResponseCollector) {
+        m_updateResponseCollector->addErrorResponse(request, error);
+        return;
+    }
+
     if (m_updateLicenseCallback) {
         m_updateLicenseCallback(false, WTF::nullopt, WTF::nullopt, WTF::nullopt, Failed);
         ASSERT(!m_updateLicenseCallback);
     }
 
-    m_currentRequest = nullptr;
+    m_currentRequest.clear();
 
     nextRequest();
 }
@@ -618,12 +875,17 @@ void CDMInstanceSessionFairPlayStreamingAVFObjC::didFailToProvideRequest(AVConte
 void CDMInstanceSessionFairPlayStreamingAVFObjC::requestDidSucceed(AVContentKeyRequest *request)
 {
     UNUSED_PARAM(request);
+    if (m_updateResponseCollector) {
+        m_updateResponseCollector->addSuccessfulResponse(request, nullptr);
+        return;
+    }
+
     if (m_updateLicenseCallback) {
         m_updateLicenseCallback(false, makeOptional(keyStatuses()), WTF::nullopt, WTF::nullopt, Succeeded);
         ASSERT(!m_updateLicenseCallback);
     }
 
-    m_currentRequest = nullptr;
+    m_currentRequest.clear();
 
     nextRequest();
 }
@@ -633,13 +895,32 @@ void CDMInstanceSessionFairPlayStreamingAVFObjC::nextRequest()
     if (m_pendingRequests.isEmpty())
         return;
 
-    RetainPtr<AVContentKeyRequest> nextRequest = m_pendingRequests.first();
+    Request nextRequest = WTFMove(m_pendingRequests.first());
     m_pendingRequests.remove(0);
 
-    if (nextRequest.get().renewsExpiringResponseData)
-        didProvideRenewingRequest(nextRequest.get());
+    if (nextRequest.isEmpty())
+        return;
+
+    if (nextRequest.size() > 1) {
+        didProvideRequests(WTFMove(nextRequest));
+        return;
+    }
+
+    auto* oneRequest = nextRequest.first().get();
+    if (oneRequest.renewsExpiringResponseData)
+        didProvideRenewingRequest(oneRequest);
     else
-        didProvideRequest(nextRequest.get());
+        didProvideRequest(oneRequest);
+}
+
+AVContentKeyRequest* CDMInstanceSessionFairPlayStreamingAVFObjC::lastKeyRequest() const
+{
+    if (m_requests.isEmpty())
+        return nil;
+    auto& lastRequest = m_requests.last();
+    if (lastRequest.isEmpty())
+        return nil;
+    return lastRequest.last().get();
 }
 
 bool CDMInstanceSessionFairPlayStreamingAVFObjC::shouldRetryRequestForReason(AVContentKeyRequest *request, NSString *reason)
@@ -684,13 +965,15 @@ CDMInstanceSession::KeyStatusVector CDMInstanceSessionFairPlayStreamingAVFObjC::
     KeyStatusVector keyStatuses;
 
     for (auto& request : m_requests) {
-        auto keyIDs = keyIDsForRequest(request.get());
-        auto status = requestStatusToCDMStatus(request.get().status);
-        if (m_outputObscured)
-            status = CDMKeyStatus::OutputRestricted;
-
-        for (auto& keyID : keyIDs)
-            keyStatuses.append({ WTFMove(keyID), status });
+        for (auto& oneRequest : request) {
+            auto keyIDs = keyIDsForRequest(oneRequest.get());
+            auto status = requestStatusToCDMStatus(oneRequest.get().status);
+            if (m_outputObscured)
+                status = CDMKeyStatus::OutputRestricted;
+
+            for (auto& keyID : keyIDs)
+                keyStatuses.append({ WTFMove(keyID), status });
+        }
     }
 
     return keyStatuses;