Bug 1883706: part 3) Implement `createHTML`, `createScript` and `createScriptURL...
[gecko.git] / browser / actors / DecoderDoctorParent.sys.mjs
blob4973a309afb82bb9dd614ba861d65892760bdacd
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/. */
7 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
8 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
10 const lazy = {};
12 ChromeUtils.defineLazyGetter(lazy, "gNavigatorBundle", function () {
13   return Services.strings.createBundle(
14     "chrome://browser/locale/browser.properties"
15   );
16 });
18 XPCOMUtils.defineLazyPreferenceGetter(
19   lazy,
20   "DEBUG_LOG",
21   "media.decoder-doctor.testing",
22   false
25 function LOG_DD(message) {
26   if (lazy.DEBUG_LOG) {
27     dump("[DecoderDoctorParent] " + message + "\n");
28   }
31 export class DecoderDoctorParent extends JSWindowActorParent {
32   getLabelForNotificationBox({ type, decoderDoctorReportId }) {
33     if (type == "platform-decoder-not-found") {
34       if (decoderDoctorReportId == "MediaWMFNeeded") {
35         return lazy.gNavigatorBundle.GetStringFromName(
36           "decoder.noHWAcceleration.message"
37         );
38       }
39       // Although this name seems generic, this is actually for not being able
40       // to find libavcodec on Linux.
41       if (decoderDoctorReportId == "MediaPlatformDecoderNotFound") {
42         return lazy.gNavigatorBundle.GetStringFromName(
43           "decoder.noCodecsLinux.message"
44         );
45       }
46     }
47     if (type == "cannot-initialize-pulseaudio") {
48       return lazy.gNavigatorBundle.GetStringFromName(
49         "decoder.noPulseAudio.message"
50       );
51     }
52     if (type == "unsupported-libavcodec" && AppConstants.platform == "linux") {
53       return lazy.gNavigatorBundle.GetStringFromName(
54         "decoder.unsupportedLibavcodec.message"
55       );
56     }
57     if (type == "decode-error") {
58       return lazy.gNavigatorBundle.GetStringFromName(
59         "decoder.decodeError.message"
60       );
61     }
62     if (type == "decode-warning") {
63       return lazy.gNavigatorBundle.GetStringFromName(
64         "decoder.decodeWarning.message"
65       );
66     }
67     return "";
68   }
70   getSumoForLearnHowButton({ type, decoderDoctorReportId }) {
71     if (
72       type == "platform-decoder-not-found" &&
73       decoderDoctorReportId == "MediaWMFNeeded"
74     ) {
75       return "fix-video-audio-problems-firefox-windows";
76     }
77     if (type == "cannot-initialize-pulseaudio") {
78       return "fix-common-audio-and-video-issues";
79     }
80     return "";
81   }
83   getEndpointForReportIssueButton(type) {
84     if (type == "decode-error" || type == "decode-warning") {
85       return Services.prefs.getStringPref(
86         "media.decoder-doctor.new-issue-endpoint",
87         ""
88       );
89     }
90     return "";
91   }
93   receiveMessage(aMessage) {
94     // The top level browsing context's embedding element should be a xul browser element.
95     let browser = this.browsingContext.top.embedderElement;
96     // The xul browser is owned by a window.
97     let window = browser?.ownerGlobal;
99     if (!browser || !window) {
100       // We don't have a browser or window so bail!
101       return;
102     }
104     let box = browser.getTabBrowser().getNotificationBox(browser);
105     let notificationId = "decoder-doctor-notification";
106     if (box.getNotificationWithValue(notificationId)) {
107       // We already have a notification showing, bail.
108       return;
109     }
111     let parsedData;
112     try {
113       parsedData = JSON.parse(aMessage.data);
114     } catch (ex) {
115       console.error(
116         "Malformed Decoder Doctor message with data: ",
117         aMessage.data
118       );
119       return;
120     }
121     // parsedData (the result of parsing the incoming 'data' json string)
122     // contains analysis information from Decoder Doctor:
123     // - 'type' is the type of issue, it determines which text to show in the
124     //   infobar.
125     // - 'isSolved' is true when the notification actually indicates the
126     //   resolution of that issue, to be reported as telemetry.
127     // - 'decoderDoctorReportId' is the Decoder Doctor issue identifier, to be
128     //   used here as key for the telemetry (counting infobar displays,
129     //   "Learn how" buttons clicks, and resolutions) and for the prefs used
130     //   to store at-issue formats.
131     // - 'formats' contains a comma-separated list of formats (or key systems)
132     //   that suffer the issue. These are kept in a pref, which the backend
133     //   uses to later find when an issue is resolved.
134     // - 'decodeIssue' is a description of the decode error/warning.
135     // - 'resourceURL' is the resource with the issue.
136     let {
137       type,
138       isSolved,
139       decoderDoctorReportId,
140       formats,
141       decodeIssue,
142       docURL,
143       resourceURL,
144     } = parsedData;
145     type = type.toLowerCase();
146     // Error out early on invalid ReportId
147     if (!/^\w+$/im.test(decoderDoctorReportId)) {
148       return;
149     }
150     LOG_DD(
151       `type=${type}, isSolved=${isSolved}, ` +
152         `decoderDoctorReportId=${decoderDoctorReportId}, formats=${formats}, ` +
153         `decodeIssue=${decodeIssue}, docURL=${docURL}, ` +
154         `resourceURL=${resourceURL}`
155     );
156     let title = this.getLabelForNotificationBox({
157       type,
158       decoderDoctorReportId,
159     });
160     if (!title) {
161       return;
162     }
164     // We keep the list of formats in prefs for the sake of the decoder itself,
165     // which reads it to determine when issues get solved for these formats.
166     // (Writing prefs from e10s content is not allowed.)
167     let formatsPref =
168       formats && "media.decoder-doctor." + decoderDoctorReportId + ".formats";
169     let buttonClickedPref =
170       "media.decoder-doctor." + decoderDoctorReportId + ".button-clicked";
171     let formatsInPref = formats && Services.prefs.getCharPref(formatsPref, "");
173     if (!isSolved) {
174       if (formats) {
175         if (!formatsInPref) {
176           Services.prefs.setCharPref(formatsPref, formats);
177         } else {
178           // Split existing formats into an array of strings.
179           let existing = formatsInPref.split(",").map(x => x.trim());
180           // Keep given formats that were not already recorded.
181           let newbies = formats
182             .split(",")
183             .map(x => x.trim())
184             .filter(x => !existing.includes(x));
185           // And rewrite pref with the added new formats (if any).
186           if (newbies.length) {
187             Services.prefs.setCharPref(
188               formatsPref,
189               existing.concat(newbies).join(", ")
190             );
191           }
192         }
193       } else if (!decodeIssue) {
194         console.error(
195           "Malformed Decoder Doctor unsolved message with no formats nor decode issue"
196         );
197         return;
198       }
200       let buttons = [];
201       let sumo = this.getSumoForLearnHowButton({ type, decoderDoctorReportId });
202       if (sumo) {
203         LOG_DD(`sumo=${sumo}`);
204         buttons.push({
205           label: lazy.gNavigatorBundle.GetStringFromName(
206             "decoder.noCodecs.button"
207           ),
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: lazy.gNavigatorBundle.GetStringFromName(
225             "decoder.decodeError.button"
226           ),
227           accessKey: lazy.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             }
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");
245             let details = { "Technical Information:": decodeIssue };
246             if (resourceURL) {
247               details["Resource:"] = resourceURL;
248             }
250             params.append("details", JSON.stringify(details));
251             window.openTrustedLinkIn(endpoint + "?" + params.toString(), "tab");
252           },
253         });
254       }
256       box.appendNotification(
257         notificationId,
258         {
259           label: title,
260           image: "", // This uses the info icon as specified below.
261           priority: box.PRIORITY_INFO_LOW,
262         },
263         buttons
264       );
265     } else if (formatsInPref) {
266       // Issue is solved, and prefs haven't been cleared yet, meaning it's the
267       // first time we get this resolution -> Clear prefs and report telemetry.
268       Services.prefs.clearUserPref(formatsPref);
269       Services.prefs.clearUserPref(buttonClickedPref);
270     }
271   }