Repository: shaka-project/shaka-player (JavaScript)

Issue:
[Android][WebView] Calls to decodingInfo or requestMediaKeySystemAccess do not resolve or reject at least without RESOURCE_PROTECTED_MEDIA_ID
<!-- NOTE: If you ignore this template, we will send it again and ask you to fill it out anyway. -->

**Have you read the [FAQ](https://bit.ly/ShakaFAQ) and checked for duplicate open issues?** Yes


**If the problem is related to FairPlay, have you read the tutorial?** N/A
<!-- NOTE: https://shaka-player-demo.appspot.com/docs/api/tutorial-fairplay.html -->


**What version of Shaka Player are you using?** v4.12.2
<!-- NOTE:
  Only some versions of the application are maintained, if your version
  is older, first update to a newer one. See:
  https://github.com/shaka-project/shaka-player/blob/main/maintained-branches.md
--->


**Can you reproduce the issue with our latest release version?** Yes


**Can you reproduce the issue with the latest code from `main`?** Yes


**Are you using the demo app or your own custom app?** Tried both, fails on both, fails even on https://shaka-player-demo.appspot.com/support.html


**If custom app, can you reproduce the issue using our demo app?** Yes


**What browser and OS are you using?** Telegram WebView on Samsung Galaxy S23 Ultra (Android 14)


**For embedded devices (smart TVs, etc.), what model and firmware version are you using?**
Mozilla/5.0 (Linux; Android 14; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.107 Mobile Safari/537.36 Telegram-Android/11.4.2 (Samsung SM-S918B; Android 14; SDK 34; HIGH)

**What are the manifest and license server URIs?** N/A
<!-- NOTE:
  You can send the URIs to <shaka-player-maintainers@googlegroups.com> instead,
  but please use GitHub and the template for the rest.
  A copy of the manifest text or an attached manifest will **not** be
  enough to reproduce your issue, and we **will** ask you to send a
  URI instead.  You can copy the URI of the demo app to send us the
  exact asset, licence server, and settings you have selected there.
  Avoid geo-blocked content if at all possible, as the contributors who diagnose
  Shaka Player issues may not be able to access it.
  If you send the URIs to email, the response time and resolution
  will be much higher.
-->


**What configuration are you using?  What is the output of `player.getNonDefaultConfiguration()`?** Default
<!-- NOTE:
  You can censor URLs to keep them private, but include them in the email.  You
  can also use JSON.stringify(player.getNonDefaultConfiguration(), null, 2) to
  print nicely on platforms that don't print objects well in the console.
  DON'T SEND '[object Object]'!
-->


**What did you do?**
<!-- Steps to reproduce the bug -->
Try to use shaka on Telegram's WebView on Android (I think without [RESOURCE_PROTECTED_MEDIA_ID](https://developer.android.com/reference/android/webkit/PermissionRequest#RESOURCE_PROTECTED_MEDIA_ID)), or load https://shaka-player-demo.appspot.com/support.html
Even the DEMO https://shaka-player-demo.appspot.com/demo/ does not work in this environment
Any call to probeSupport gets hanged.

**What did you expect to happen?**
Calls to probeSupport not to get hanged on this environment even if the permission is not set.

**What actually happened?**
<!-- A clear and concise description of what the bug is -->
<!-- If applicable, you may add screenshots to help explain your problem. -->
Calls to probeSupport get hanged.
Promise calls to decodingInfo or requestMediaKeySystemAccess do not resolve or reject on this environment.

**Are you planning send a PR to fix it?** Yes




Fix (patch):
diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js
index 5ccd43d57d..606853ee02 100644
--- a/lib/media/drm_engine.js
+++ b/lib/media/drm_engine.js
@@ -15,6 +15,7 @@ goog.require('shaka.util.DrmUtils');
 goog.require('shaka.util.Error');
 goog.require('shaka.util.EventManager');
 goog.require('shaka.util.FakeEvent');
+goog.require('shaka.util.Functional');
 goog.require('shaka.util.IDestroyable');
 goog.require('shaka.util.Iterables');
 goog.require('shaka.util.ManifestParserUtils');
@@ -1870,9 +1871,16 @@ shaka.media.DrmEngine = class {
         offlineConfig.sessionTypes = ['persistent-license'];
 
         const configs = [offlineConfig, basicConfig];
-
-        const access = await navigator.requestMediaKeySystemAccess(
-            keySystem, configs);
+        // On some (Android) WebView environments,
+        // requestMediaKeySystemAccess will
+        // not resolve or reject, at least if RESOURCE_PROTECTED_MEDIA_ID
+        // is not set.  This is a workaround for that issue.
+        const TIMEOUT_FOR_CHECKACCESS_IN_SECONDS = 1;
+        const access =
+          await shaka.util.Functional.promiseWithTimeout(
+              TIMEOUT_FOR_CHECKACCESS_IN_SECONDS,
+              navigator.requestMediaKeySystemAccess(keySystem, configs),
+          );
         await processMediaKeySystemAccess(keySystem, access);
       } catch (error) {}  // Ignore errors.
     };
@@ -1907,13 +1915,22 @@ shaka.media.DrmEngine = class {
             },
           },
         };
-
+        // On some (Android) WebView environments, decodingInfo will
+        // not resolve or reject, at least if RESOURCE_PROTECTED_MEDIA_ID
+        // is not set.  This is a workaround for that issue.
+        const TIMEOUT_FOR_DECODING_INFO_IN_SECONDS = 1;
         const decodingInfo =
-            await navigator.mediaCapabilities.decodingInfo(decodingConfig);
+          await shaka.util.Functional.promiseWithTimeout(
+              TIMEOUT_FOR_DECODING_INFO_IN_SECONDS,
+              navigator.mediaCapabilities.decodingInfo(decodingConfig),
+          );
 
         const access = decodingInfo.keySystemAccess;
         await processMediaKeySystemAccess(keySystem, access);
-      } catch (error) {}  // Ignore errors.
+      } catch (error) {
+        // Ignore errors.
+        shaka.log.v2('Failed to probe support for', keySystem, error);
+      }
     };
 
     // Initialize the support structure for each key system.
diff --git a/lib/util/functional.js b/lib/util/functional.js
index 694d99d49c..06722067b4 100644
--- a/lib/util/functional.js
+++ b/lib/util/functional.js
@@ -6,6 +6,8 @@
 
 goog.provide('shaka.util.Functional');
 
+goog.require('shaka.util.Timer');
+
 /**
  * @summary A set of functional utility functions.
  */
@@ -67,4 +69,29 @@ shaka.util.Functional = class {
   static isNotNull(value) {
     return value != null;
   }
+
+  /**
+   * Returns a Promise which is resolved only if |asyncProcess| is resolved, and
+   * only if it is resolved in less than |seconds| seconds.
+   *
+   * If the returned Promise is resolved, it returns the same value as
+   * |asyncProcess|.
+   *
+   * If |asyncProcess| fails, the returned Promise is rejected.
+   * If |asyncProcess| takes too long, the returned Promise is rejected, but
+   * |asyncProcess| is still allowed to complete.
+   *
+   * @param {number} seconds
+   * @param {!Promise.<T>} asyncProcess
+   * @return {!Promise.<T>}
+   * @template T
+   */
+  static promiseWithTimeout(seconds, asyncProcess) {
+    return Promise.race([
+      asyncProcess,
+      new Promise(((_, reject) => {
+        new shaka.util.Timer(reject).tickAfter(seconds);
+      })),
+    ]);
+  }
 };
diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js
index 6dca1ff98d..8ec728e6b6 100644
--- a/lib/util/stream_utils.js
+++ b/lib/util/stream_utils.js
@@ -727,7 +727,14 @@ shaka.util.StreamUtils = class {
               mimeType, audioCodec);
         }
         promises.push(new Promise((resolve, reject) => {
-          navigator.mediaCapabilities.decodingInfo(copy).then((res) => {
+          // On some (Android) WebView environments, decodingInfo will
+          // not resolve or reject, at least if RESOURCE_PROTECTED_MEDIA_ID
+          // is not set.  This is a workaround for that issue.
+          const TIMEOUT_FOR_DECODING_INFO_IN_SECONDS = 1;
+          shaka.util.Functional.promiseWithTimeout(
+              TIMEOUT_FOR_DECODING_INFO_IN_SECONDS,
+              navigator.mediaCapabilities.decodingInfo(copy),
+          ).then((res) => {
             resolve(res);
           }).catch(reject);
         }));