1/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */
2/* vim: set ts=2 sw=2 sts=2 et tw=80: */
3/* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7"use strict";
8
9var EXPORTED_SYMBOLS = ["DecoderDoctorParent"];
10
11const { AppConstants } = ChromeUtils.import(
12  "resource://gre/modules/AppConstants.jsm"
13);
14const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
15const { XPCOMUtils } = ChromeUtils.import(
16  "resource://gre/modules/XPCOMUtils.jsm"
17);
18
19XPCOMUtils.defineLazyGetter(this, "gNavigatorBundle", function() {
20  return Services.strings.createBundle(
21    "chrome://browser/locale/browser.properties"
22  );
23});
24
25XPCOMUtils.defineLazyPreferenceGetter(
26  this,
27  "DEBUG_LOG",
28  "media.decoder-doctor.testing",
29  false
30);
31
32function LOG_DD(message) {
33  if (DEBUG_LOG) {
34    dump("[DecoderDoctorParent] " + message + "\n");
35  }
36}
37
38class DecoderDoctorParent extends JSWindowActorParent {
39  getLabelForNotificationBox({ type, decoderDoctorReportId }) {
40    if (type == "platform-decoder-not-found") {
41      if (decoderDoctorReportId == "MediaWMFNeeded") {
42        return gNavigatorBundle.GetStringFromName(
43          "decoder.noHWAcceleration.message"
44        );
45      }
46      // Although this name seems generic, this is actually for not being able
47      // to find libavcodec on Linux.
48      if (decoderDoctorReportId == "MediaPlatformDecoderNotFound") {
49        return gNavigatorBundle.GetStringFromName(
50          "decoder.noCodecsLinux.message"
51        );
52      }
53    }
54    if (type == "cannot-initialize-pulseaudio") {
55      return gNavigatorBundle.GetStringFromName("decoder.noPulseAudio.message");
56    }
57    if (type == "unsupported-libavcodec" && AppConstants.platform == "linux") {
58      return gNavigatorBundle.GetStringFromName(
59        "decoder.unsupportedLibavcodec.message"
60      );
61    }
62    if (type == "decode-error") {
63      return gNavigatorBundle.GetStringFromName("decoder.decodeError.message");
64    }
65    if (type == "decode-warning") {
66      return gNavigatorBundle.GetStringFromName(
67        "decoder.decodeWarning.message"
68      );
69    }
70    return "";
71  }
72
73  getSumoForLearnHowButton({ type, decoderDoctorReportId }) {
74    if (
75      type == "platform-decoder-not-found" &&
76      decoderDoctorReportId == "MediaWMFNeeded"
77    ) {
78      return "fix-video-audio-problems-firefox-windows";
79    }
80    if (type == "cannot-initialize-pulseaudio") {
81      return "fix-common-audio-and-video-issues";
82    }
83    return "";
84  }
85
86  getEndpointForReportIssueButton(type) {
87    if (type == "decode-error" || type == "decode-warning") {
88      return Services.prefs.getStringPref(
89        "media.decoder-doctor.new-issue-endpoint",
90        ""
91      );
92    }
93    return "";
94  }
95
96  receiveMessage(aMessage) {
97    // The top level browsing context's embedding element should be a xul browser element.
98    let browser = this.browsingContext.top.embedderElement;
99    // The xul browser is owned by a window.
100    let window = browser?.ownerGlobal;
101
102    if (!browser || !window) {
103      // We don't have a browser or window so bail!
104      return;
105    }
106
107    let box = browser.getTabBrowser().getNotificationBox(browser);
108    let notificationId = "decoder-doctor-notification";
109    if (box.getNotificationWithValue(notificationId)) {
110      // We already have a notification showing, bail.
111      return;
112    }
113
114    let parsedData;
115    try {
116      parsedData = JSON.parse(aMessage.data);
117    } catch (ex) {
118      Cu.reportError(
119        "Malformed Decoder Doctor message with data: " + aMessage.data
120      );
121      return;
122    }
123    // parsedData (the result of parsing the incoming 'data' json string)
124    // contains analysis information from Decoder Doctor:
125    // - 'type' is the type of issue, it determines which text to show in the
126    //   infobar.
127    // - 'isSolved' is true when the notification actually indicates the
128    //   resolution of that issue, to be reported as telemetry.
129    // - 'decoderDoctorReportId' is the Decoder Doctor issue identifier, to be
130    //   used here as key for the telemetry (counting infobar displays,
131    //   "Learn how" buttons clicks, and resolutions) and for the prefs used
132    //   to store at-issue formats.
133    // - 'formats' contains a comma-separated list of formats (or key systems)
134    //   that suffer the issue. These are kept in a pref, which the backend
135    //   uses to later find when an issue is resolved.
136    // - 'decodeIssue' is a description of the decode error/warning.
137    // - 'resourceURL' is the resource with the issue.
138    let {
139      type,
140      isSolved,
141      decoderDoctorReportId,
142      formats,
143      decodeIssue,
144      docURL,
145      resourceURL,
146    } = parsedData;
147    type = type.toLowerCase();
148    // Error out early on invalid ReportId
149    if (!/^\w+$/im.test(decoderDoctorReportId)) {
150      return;
151    }
152    LOG_DD(
153      `type=${type}, isSolved=${isSolved}, ` +
154        `decoderDoctorReportId=${decoderDoctorReportId}, formats=${formats}, ` +
155        `decodeIssue=${decodeIssue}, docURL=${docURL}, ` +
156        `resourceURL=${resourceURL}`
157    );
158    let title = this.getLabelForNotificationBox({
159      type,
160      decoderDoctorReportId,
161    });
162    if (!title) {
163      return;
164    }
165
166    // We keep the list of formats in prefs for the sake of the decoder itself,
167    // which reads it to determine when issues get solved for these formats.
168    // (Writing prefs from e10s content is not allowed.)
169    let formatsPref =
170      formats && "media.decoder-doctor." + decoderDoctorReportId + ".formats";
171    let buttonClickedPref =
172      "media.decoder-doctor." + decoderDoctorReportId + ".button-clicked";
173    let formatsInPref = formats && Services.prefs.getCharPref(formatsPref, "");
174
175    if (!isSolved) {
176      if (formats) {
177        if (!formatsInPref) {
178          Services.prefs.setCharPref(formatsPref, formats);
179        } else {
180          // Split existing formats into an array of strings.
181          let existing = formatsInPref.split(",").map(x => x.trim());
182          // Keep given formats that were not already recorded.
183          let newbies = formats
184            .split(",")
185            .map(x => x.trim())
186            .filter(x => !existing.includes(x));
187          // And rewrite pref with the added new formats (if any).
188          if (newbies.length) {
189            Services.prefs.setCharPref(
190              formatsPref,
191              existing.concat(newbies).join(", ")
192            );
193          }
194        }
195      } else if (!decodeIssue) {
196        Cu.reportError(
197          "Malformed Decoder Doctor unsolved message with no formats nor decode issue"
198        );
199        return;
200      }
201
202      let buttons = [];
203      let sumo = this.getSumoForLearnHowButton({ type, decoderDoctorReportId });
204      if (sumo) {
205        LOG_DD(`sumo=${sumo}`);
206        buttons.push({
207          label: gNavigatorBundle.GetStringFromName("decoder.noCodecs.button"),
208          supportPage: sumo,
209          callback() {
210            let clickedInPref = Services.prefs.getBoolPref(
211              buttonClickedPref,
212              false
213            );
214            if (!clickedInPref) {
215              Services.prefs.setBoolPref(buttonClickedPref, true);
216            }
217          },
218        });
219      }
220      let endpoint = this.getEndpointForReportIssueButton(type);
221      if (endpoint) {
222        LOG_DD(`endpoint=${endpoint}`);
223        buttons.push({
224          label: gNavigatorBundle.GetStringFromName(
225            "decoder.decodeError.button"
226          ),
227          accessKey: gNavigatorBundle.GetStringFromName(
228            "decoder.decodeError.accesskey"
229          ),
230          callback() {
231            let clickedInPref = Services.prefs.getBoolPref(
232              buttonClickedPref,
233              false
234            );
235            if (!clickedInPref) {
236              Services.prefs.setBoolPref(buttonClickedPref, true);
237            }
238
239            let params = new URLSearchParams();
240            params.append("url", docURL);
241            params.append("label", "type-media");
242            params.append("problem_type", "video_bug");
243            params.append("src", "media-decode-error");
244
245            let details = { "Technical Information:": decodeIssue };
246            if (resourceURL) {
247              details["Resource:"] = resourceURL;
248            }
249
250            params.append("details", JSON.stringify(details));
251            window.openTrustedLinkIn(endpoint + "?" + params.toString(), "tab");
252          },
253        });
254      }
255
256      box.appendNotification(
257        title,
258        notificationId,
259        "", // This uses the info icon as specified below.
260        box.PRIORITY_INFO_LOW,
261        buttons
262      );
263    } else if (formatsInPref) {
264      // Issue is solved, and prefs haven't been cleared yet, meaning it's the
265      // first time we get this resolution -> Clear prefs and report telemetry.
266      Services.prefs.clearUserPref(formatsPref);
267      Services.prefs.clearUserPref(buttonClickedPref);
268    }
269  }
270}
271