Web-bugs: www.pscp.tv - video or audio doesn't play

Created on 18 Jul 2017  路  10Comments  路  Source: webcompat/web-bugs



URL: https://www.pscp.tv/
Problem type: Video or audio doesn't play
Description: the streming site is not working properly
Steps to Reproduce
Browser / Version: Firefox 56.0a1
Operating System: android 6.0
Tested Another Browser: Yes

_From webcompat.com with 鉂わ笍_

browser-firefox-mobile priority-important

Most helpful comment

That was fun to read @wisniewskit! Feel free to reach out directly in the future, I've got a fix going out for Firefox on Android with a minimum version of 54. If you know the minimum version we can specify I'll be happy to update that as well!

All 10 comments

Yep, in Chrome Mobile I can see the streams at least attempt to load. In Fennec, you just get a splash screen and a menu item links to the app.

(Possibly related to HLS detection?)

I open a channel at https://www.pscp.tv/w/1YpKkmXnEDmJj?channel=travel

The video element on Firefox desktop is:

<video preload="metadata" style="width: 100%; height: 100%; position: absolute; left: 0px; right: 0px; top: 0px; bottom: 0px; z-index: 1;" webkit-playsinline="" src="blob:https://www.pscp.tv/c49f11c8-5e9f-4d45-9ddb-67c4de07cf9b"></video>

On Fennec it shows a notification to open on periscope app but I can watch the video on Chrome mobile.

This is a problem with client-side userAgent sniffing; over-riding the value of navigator.userAgent to a Chrome UA does the trick, and Firefox on Android shows the page similarly.

However, their UA sniffs are highly convoluted and intricate, and I can't figure out which of the many places they use them are the right ones to fix. I suspect that this is one they may just have to figure out for themselves.

@denschub, if you're feeling brave, the two files I noticed which have UA-sniffs are main.js and vendor.js. Inserting this code into the top of the vendor.js file proves that it's just a UA-sniff:

Object.defineProperty(navigator, "userAgent", {
  get: function() {
    return "Android Chrome/56";
  }
});

They also check other navigator properties like appVersion and vendor, but it doesn't seem necessary to override them in order for the page to load similarly in Fennec as Chrome.

The issue is still reproducible.

Tested with:
Browser / Version: Firefox Mobile Nightly 60.0a1 (2018-03-07)
Operating System: Google Pixel (Android 8.0.0) - 1080 x 1920 pixels (~441 ppi pixel density), Samsung Galaxy S7 Edge (Android 7.0) - Resolution 1440 x 2560 pixels (~534 ppi pixel density)

I decided to re-diagnose this one completely fresh, because I remember this being a royal pain and didn't want to relive any of the less savory moments.

This time, I started by checking if spoofing as Firefox/Android while using the desktop responsive mode would trigger the same mobile UI with the "please use the app" dialog. Thankfully it did, which sped up this diagnosis a great deal.

I then did the diagnosis proper by first finding where the text from the "please use the app" dialog came from, and working my way backward. The script defining that text put it in a variable named app.MOBILE, but that's a pretty generic name so I instead searched for another variable that script was naming that seemed to be involved in UA sniffing, UNSUPPORTED_360, hoping that these were used by the same decision-making code.

This paid off, as only one script seemed to reference it. As anticipated, that script has a ton of references to navigator.userAgent, but changing all of them in order to spoof a mobile Chrome UA worked, so I was on the right track. And so I roughly did a binary search of sorts to find out which navigator.userAgent reference, when spoofed, let Firefox through. Luckily that didn't take very long (and mercifully it didn't end up being a complex combination of references).

This is the method I turned up:

    $.dispatch((0, C.trackEvent)(N.FIRST_RENDER_ACTION)), (0, A.ensureIntl)(ee).then(function(e) {
        var t = (e.locale, e.messages),
            n = e.reactIntlLocale;
        try {
            var r = (0, x.default)($, new y.default({
                    store: $
                }), L.apiErrorHandler),
                a = (0, v.default)(r),
                i = (0, w.default)({
                    api: r
                });
            B.default.runAfterInteractions(null, "PlaybackMetaStoreTransmit").then(function() {
                E.PlaybackMetaStore.transmit(a)
            }), (0, c.match)({
                history: I.default,
                routes: i

            }, function(e, r, i) {
                i && (0, b.withLoadedComponents)(s.default.createElement(p.IntlProvider, {
                    locale: n,
                    defaultLocale: "en",
                    messages: t
                }, s.default.createElement(d.Provider, {
                    store: $
                }, s.default.createElement(h.ApiProvider, {
                    api: a
                }, s.default.createElement(T.EnvironmentProvider, {
                    ua: navigator.userAgent
                }, s.default.createElement(c.Router, (0, o.default)({
                    history: re
                }, i))))))).then(function(e) {
                    var t = e.loadedComponents;
                    te(t), Z.done(), (0, X.default)({
                        store: $,
                        location: window.location
                    })
                }) 
            })      
        } catch (e) {
            ne("RENDER_FAILURE", e), Z.done()
        }   

But that's not the end of the road, as they're feeding the value of navigator.userAgent into some other function (a big warning sign that things were about to get tricky). Both s.default.createElement and T.EnvironmentProvider were both functions, but only the latter was interesting:

function t(e) {
  (0, d.default)(this, t);
  var n = (0, m.default)(this, (t.__proto__ || (0, l.default)(t)).call(this, e));
  return n.periscopeEnvironment = e.periscopeEnvironment || new O.UserAgentDetector(e.ua), n
}

By then my "oh no" detector was going crazy, as any complex UA-sniffing object like this usually ends up being referenced all over the place. The saving grace was that when I ran JSON.stringify on it to see how complicated it was, there were only a few interesting variables it played with:

{ // for Chrome Mobile
  "ua":"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Mobile Safari/537.36",
  "browser":{"name":"Chrome","version":"61.0.3163.79","major":"61"},
  "engine":{"version":"537.36","name":"WebKit"},
  "os":{"name":"Android","version":"6.0"},
  "device":{"model":"Nexus 5","vendor":"LG","type":"mobile"},
  "cpu":{}
}

{ // for Firefox mobile         
  "ua":"Mozilla/5.0 (Android 7.0; Mobile; rv:57.0) Gecko/57.0 Firefox/57.0",
  "browser":{"name":"Firefox","version":"57.0","major":"57"},
  "engine":{"version":"57.0","name":"Gecko"},
  "os":{"name":"Android","version":"7.0"},
  "device":{"type":"mobile"},
  "cpu":{} 
}

That gave me hope that by simply starting with the Firefox values and replacing one at a time, I might find which one was involved in the decision. Which worked; it turned out that only the browser value influenced whether the popup was shown.

My luck held up from there, as there are only a few references to the literal string "Chrome" in the same script, and in fact all in the same UserAgentDetector object. This was the one involved in the final decision:

}, {
    key: "chromeVersion",
    value: function(e) {
        return this.browserVersion("Chrome", e)
    }
}, {

Which quickly lead me to the final culprit, still in the UserAgentDetector object:

}, {    
    key: "hasMobileInlineAutoplayVideo",
    value: function() {
        return this.iosVersion(10) && !this.chromeVersion() || this.android() && this.chromeVersion(53) || this.ios() && this.chromeVersion(54)
    }
}, {

And of course they're checking for specific major IOS versions and Android Chrome versions, rather than using video.canPlayType. If that's really the way they want to do things, then they should at least add Android Firefox to that list:

  return this.iosVersion(10) && !this.chromeVersion() || this.android() && this.chromeVersion(53) || this.ios() && this.chromeVersion(54) || this.android() && this.browserVersion("Firefox", 54)

That does the trick for Firefox on my phone, and I don't see anything other obvious bits in that UA-sniffer that could use an adjustment.

Thanks @wisniewskit. Reaching out to Twitter via our mailing list.

That was fun to read @wisniewskit! Feel free to reach out directly in the future, I've got a fix going out for Firefox on Android with a minimum version of 54. If you know the minimum version we can specify I'll be happy to update that as well!

Thanks a ton, @magus!

To be honest, using canPlayType is usually the safer bet if you know which codecs your videos use (since support for codecs varies far more on the device and mobile OS than it does the browser version).

I only chose 54 arbitrarily, just in case you still had Firefox for Android users lagging that far behind. It would be better to take a quick peek into your user analytics (assuming you have them of course), and use a sensible version based on what your users actually use. You might even be able to just use the current stable version (59).

PS: also feel free to check with us if you have any webcompat issues like this you're unsure about (whether it's related to Firefox or not). We're happy to help!

Should be live in production, feel free to close this ticket once you confirm!

Indeed, it's working for me now on my phone. Thanks again, @magus!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

webcompat-bot picture webcompat-bot  路  4Comments

GeorgeWL picture GeorgeWL  路  5Comments

webcompat-bot picture webcompat-bot  路  4Comments

Ezio916 picture Ezio916  路  4Comments

Gravydigger picture Gravydigger  路  4Comments