common.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. /*
  2. Movie App - Common Configuration
  3. If the app folder is renamed, update APP_NAME below and all paths
  4. will automatically adjust everywhere this file is included via requirepkg().
  5. */
  6. // ── App identity ─────────────────────────────────────────────────────────────
  7. var APP_NAME = "Movie";
  8. var BACKEND_PATH = APP_NAME + "/backend/";
  9. // ── Server API endpoints (relative from any page in this app) ────────────────
  10. var MEDIA_API = "../media"; // ?file=<vpath> streams a file
  11. var TRANSCODE_API = "../media/transcode"; // ?file
  12. var HLS_API = "../media/hls"; // ?file same transcode, as an HLS playlist
  13. var PROBE_API = "../media/probe/"; // ?file real codecs + playability
  14. var STORYBOARD_API = "../media/storyboard/"; // ?file[&image=1] scrub previews
  15. var SUBTITLE_API = "../media/subtitles/"; // ?file[&track=n|&font=n] embedded tracks
  16. var AGI_INTERFACE = "../system/ajgi/interface?script=";
  17. // ── Script paths (used when calling ao_module_agirun from the frontend) ──────
  18. var SCRIPT_GET_LIBRARY = BACKEND_PATH + "getLibrary.js";
  19. var SCRIPT_GET_LIBRARY_CACHE = BACKEND_PATH + "getLibraryCache.js";
  20. var SCRIPT_GET_EPISODES = BACKEND_PATH + "getEpisodes.js";
  21. var SCRIPT_GET_THUMBNAIL = BACKEND_PATH + "getThumbnail.js";
  22. var SCRIPT_LIST_FOLDER = BACKEND_PATH + "listFolder.js";
  23. var SCRIPT_GET_MOVIE_INFO = BACKEND_PATH + "getMovieInfo.js";
  24. var SCRIPT_DISABLE_MOVIE_INFO = BACKEND_PATH + "disableMovieInfo.js";
  25. var SCRIPT_GET_WATCHTIME = BACKEND_PATH + "getWatchTime.js";
  26. var SCRIPT_SET_WATCHTIME = BACKEND_PATH + "setWatchTime.js";
  27. var SCRIPT_GET_INDEX_STATS = BACKEND_PATH + "getIndexStats.js";
  28. var SCRIPT_CLEAR_INDEX = BACKEND_PATH + "clearIndex.js";
  29. // ── Streaming mode ───────────────────────────────────────────────────────────
  30. // Formats the browser cannot decode are transcoded on the fly, and there are
  31. // two ways to deliver that transcode:
  32. //
  33. // mp4 a single fragmented MP4 streamed down one response. The long-standing
  34. // default: it starts fastest and costs the server nothing but the ffmpeg
  35. // process. It cannot answer byte-range requests, which WebKit clients
  36. // (Safari, and every browser on iOS) require before they will play
  37. // anything, so on those it fails outright.
  38. // hls the same transcode cut into segments behind a playlist. Every segment
  39. // is a finite, seekable file, so WebKit plays it and seeking inside the
  40. // transcoded window no longer restarts the stream.
  41. //
  42. // "auto" picks hls on WebKit and mp4 everywhere else, so nothing changes for
  43. // browsers that were already working.
  44. var STREAM_MODE_KEY = "movie_stream_mode";
  45. function isWebKitClient() {
  46. var ua = navigator.userAgent;
  47. // On iOS every browser is WebKit underneath, whatever it calls itself.
  48. if (/CriOS|FxiOS/.test(ua)) { return true; }
  49. // Chrome, Edge and Opera all carry "Safari" in their desktop UA.
  50. if (/Chrome\/|Chromium|Edg\/|OPR\/|Firefox\//.test(ua)) { return false; }
  51. return /Safari/.test(ua);
  52. }
  53. function getStreamMode() {
  54. var mode = localStorage.getItem(STREAM_MODE_KEY);
  55. return (mode === "mp4" || mode === "hls") ? mode : "auto";
  56. }
  57. function setStreamMode(mode) {
  58. if (mode !== "mp4" && mode !== "hls") { mode = "auto"; }
  59. localStorage.setItem(STREAM_MODE_KEY, mode);
  60. }
  61. // Whether the current preference resolves to HLS for this browser.
  62. function usingHLS() {
  63. var mode = getStreamMode();
  64. if (mode === "hls") { return true; }
  65. if (mode === "mp4") { return false; }
  66. return isWebKitClient();
  67. }
  68. // WebKit plays HLS natively. Everywhere else it is played through Media Source
  69. // Extensions: hls.js if someone has vendored it into web/script/, otherwise the
  70. // built-in MSE player in web/script/hlsmse.js, which is enough because the
  71. // server emits single-variant fragmented-MP4 playlists.
  72. function nativeHLSSupported(videoEl) {
  73. if (!videoEl || !videoEl.canPlayType) { return false; }
  74. if (videoEl.canPlayType("application/vnd.apple.mpegurl") === "") { return false; }
  75. // canPlayType alone cannot be trusted here: Chrome answers "maybe" for the
  76. // HLS MIME type and then fails to play the playlist. WebKit is the only
  77. // engine with a real native HLS pipeline, so the answer only counts there —
  78. // everyone else goes through Media Source instead.
  79. return isWebKitClient();
  80. }
  81. function hlsPlaybackSupported(videoEl) {
  82. if (nativeHLSSupported(videoEl)) { return true; }
  83. if (window.Hls && window.Hls.isSupported()) { return true; }
  84. return !!(window.MovieHLS && window.MovieHLS.isSupported());
  85. }
  86. // ── Direct play vs transcode ─────────────────────────────────────────────────
  87. // The container extension is only a hint. An .mp4 may hold HEVC, AV1 or 10-bit
  88. // H.264, none of which most browsers decode — playing those directly fails with
  89. // a bare decode error (NS_ERROR_DOM_MEDIA_METADATA_ERR on Firefox) instead of
  90. // being transcoded. So the server is asked what the file really contains.
  91. var WEB_PLAYABLE_EXTENSIONS = ["mp4", "webm", "ogg", "m4v"];
  92. var _codecProbeCache = {};
  93. function isWebPlayableExtension(ext) {
  94. return WEB_PLAYABLE_EXTENSIONS.indexOf(String(ext || "").toLowerCase()) !== -1;
  95. }
  96. // Decide whether a file can be handed to the browser as-is. Calls back with
  97. // true for direct play, false to transcode.
  98. //
  99. // Only web-native containers are probed: anything else is transcoded regardless,
  100. // so there is nothing to learn. Results are cached per file, making repeat plays
  101. // and episode changes free.
  102. function resolveDirectPlay(filepath, ext, callback) {
  103. if (!isWebPlayableExtension(ext)) { callback(false); return; }
  104. if (Object.prototype.hasOwnProperty.call(_codecProbeCache, filepath)) {
  105. callback(_codecProbeCache[filepath]);
  106. return;
  107. }
  108. fetch(PROBE_API + "?file=" + encodeURIComponent(filepath), { credentials: "same-origin" })
  109. .then(function (r) { return r.json(); })
  110. .then(function (info) {
  111. // A probe error (no ffmpeg, remote file system) leaves the old
  112. // extension-based behaviour in place rather than blocking playback.
  113. var direct = (info && !info.error) ? !!info.directPlay : true;
  114. _codecProbeCache[filepath] = direct;
  115. callback(direct);
  116. })
  117. .catch(function () { callback(true); });
  118. }
  119. // Safety net for direct playback: the probe can still be wrong for a browser
  120. // missing a platform decoder, so a decode failure retries as a transcode rather
  121. // than leaving the viewer on a dead player.
  122. function onDirectPlaybackFailure(videoEl, onFallback) {
  123. clearDirectPlaybackWatch(videoEl);
  124. var handler = function () {
  125. clearDirectPlaybackWatch(videoEl);
  126. var err = videoEl.error;
  127. // 3 = MEDIA_ERR_DECODE, 4 = MEDIA_ERR_SRC_NOT_SUPPORTED. A network
  128. // abort is not a codec problem and must not trigger a transcode.
  129. if (!err || (err.code !== 3 && err.code !== 4)) { return; }
  130. if (typeof onFallback === "function") { onFallback(); }
  131. };
  132. videoEl._directFallback = handler;
  133. videoEl.addEventListener("error", handler);
  134. }
  135. function clearDirectPlaybackWatch(videoEl) {
  136. if (videoEl && videoEl._directFallback) {
  137. videoEl.removeEventListener("error", videoEl._directFallback);
  138. videoEl._directFallback = null;
  139. }
  140. }
  141. // Build the streaming URL for a transcoded file, honouring the current mode.
  142. // startSeconds restarts the transcode at an offset; the resulting stream always
  143. // begins at zero, so callers track the offset separately.
  144. function transcodeStreamURL(filepath, startSeconds) {
  145. var base = usingHLS() ? HLS_API : TRANSCODE_API;
  146. var url = base + "?file=" + encodeURIComponent(filepath);
  147. if (startSeconds && startSeconds > 0.001) {
  148. url += "&start=" + startSeconds.toFixed(3);
  149. }
  150. return url;
  151. }
  152. // Point a <video> at a stream URL. HLS on a browser without native support is
  153. // routed through hls.js when it is available. Returns false when the stream
  154. // cannot be played at all, so the caller can say so rather than hang.
  155. function attachTranscodeStream(videoEl, url, onError) {
  156. detachTranscodeStream(videoEl);
  157. var isPlaylist = url.indexOf(HLS_API) === 0;
  158. if (isPlaylist && !nativeHLSSupported(videoEl)) {
  159. if (window.Hls && window.Hls.isSupported()) {
  160. var hls = new window.Hls({ enableWorker: true });
  161. videoEl._hlsInstance = hls;
  162. hls.loadSource(url);
  163. hls.attachMedia(videoEl);
  164. return true;
  165. }
  166. if (window.MovieHLS && window.MovieHLS.isSupported()) {
  167. videoEl._mseInstance = window.MovieHLS.attach(videoEl, url, {
  168. onError: function (reason, err) {
  169. if (typeof onError === "function") { onError(reason, err); }
  170. }
  171. });
  172. return true;
  173. }
  174. return false;
  175. }
  176. videoEl.src = url;
  177. videoEl.load();
  178. return true;
  179. }
  180. // Release whichever player is currently bound to the element. Always call this
  181. // before pointing a <video> somewhere new, or an MSE attachment keeps feeding
  182. // segments into an element that has moved on.
  183. function detachTranscodeStream(videoEl) {
  184. if (!videoEl) { return; }
  185. if (videoEl._hlsInstance) {
  186. try { videoEl._hlsInstance.destroy(); } catch (e) {}
  187. videoEl._hlsInstance = null;
  188. }
  189. if (videoEl._mseInstance) {
  190. try { videoEl._mseInstance.destroy(); } catch (e) {}
  191. videoEl._mseInstance = null;
  192. }
  193. }
  194. // ── Scanner settings ─────────────────────────────────────────────────────────
  195. var VALID_VIDEO_FORMATS = ["mp4", "webm", "ogg", "mkv", "avi", "mov", "m4v", "wmv", "flv", "rmvb", "ts"];
  196. var SKIP_ROOT_PREFIXES = ["tmp:/", "trash:/"]; // roots to skip entirely
  197. var VIDEO_FOLDER_NAME = "Video"; // expected folder inside each root
  198. var MOVIE_FOLDER_NAMES = ["movie", "movies"]; // folder names (case-insensitive) treated as movie containers
  199. var ANIME_FOLDER_NAMES = ["anime"]; // folder names (case-insensitive) treated as anime containers