(function () {
    "use strict";

    var HLS_JS_CDN = "js/vendor/hls.min.js";
    var MPEGTS_JS_CDN = "js/vendor/mpegts.min.js";
    var HTML5_SUBTITLE_ID_OFFSET = 10000;
    var hlsLoadState = "idle";
    var hlsLoadCallbacks = [];
    var mpegTsLoadState = "idle";
    var mpegTsLoadCallbacks = [];

    function formatTime(seconds) {
        if (!isFinite(seconds) || seconds < 0) {
            return "00:00";
        }

        var totalSeconds = Math.floor(seconds);
        var hours = Math.floor(totalSeconds / 3600);
        var minutes = Math.floor((totalSeconds % 3600) / 60);
        var secs = totalSeconds % 60;

        function pad(value) {
            return value < 10 ? "0" + value : String(value);
        }

        if (hours > 0) {
            return hours + ":" + pad(minutes) + ":" + pad(secs);
        }

        return pad(minutes) + ":" + pad(secs);
    }

    function getUrlPath(url) {
        return String(url || "").split("?")[0].toLowerCase();
    }

    function getUrlExtension(url) {
        var match = getUrlPath(url).match(/\.([a-z0-9]+)$/i);
        return match ? match[1].toLowerCase() : "";
    }

    function isTizenAvplayAvailable() {
        return typeof webapis !== "undefined" &&
            webapis &&
            webapis.avplay;
    }

    function isHlsSource(item) {
        return getUrlExtension(item && item.url) === "m3u8" || (item && item.type === "hls");
    }

    function isMp4Source(item) {
        return getUrlExtension(item && item.url) === "mp4" || (item && item.type === "mp4");
    }

    function isMkvSource(item) {
        return getUrlExtension(item && item.url) === "mkv" || (item && item.type === "mkv");
    }

    function isVodFileSource(item) {
        return /^(mp4|mkv|avi|mov|m4v|webm)$/.test(getUrlExtension(item && item.url)) || isMp4Source(item);
    }

    function isMpegTsSource(item) {
        return getUrlExtension(item && item.url) === "ts" || (item && item.type === "mpegts");
    }

    function isXtreamVodUrl(url) {
        return /\/(?:movie|series)\//i.test(String(url || ""));
    }

    function isXtreamLiveUrl(url) {
        return /\/live\//i.test(String(url || ""));
    }

    function isXtreamStreamUrl(url) {
        return /\/(?:live|movie|series)\//i.test(String(url || ""));
    }

    function isLivePlaybackItem(item) {
        return !!(item && (item.section === "live" || isXtreamLiveUrl(item.url) || item.isLive));
    }

    function canUseAvplay(item) {
        return !!(isTizenAvplayAvailable() &&
            item &&
            (isVodFileSource(item) || isMpegTsSource(item) || isHlsSource(item) || isXtreamStreamUrl(item.url)));
    }

    function makeSameUrlWithExtension(url, extension) {
        return String(url || "").replace(/\.(mp4|m3u8|mkv|ts|avi)(\?.*)?$/i, "." + extension + "$2");
    }

    function makeUrlWithoutExtension(url) {
        return String(url || "").replace(/\.(mp4|m3u8|mkv|ts|avi)(\?.*)?$/i, "$2");
    }

    function getXtreamExtensionFallbacks(url) {
        var currentExt = getUrlExtension(url);
        var order = ["mp4", "m3u8", "mkv", "ts", "avi"];
        var fallbacks = [];
        var i;
        var candidate;

        if (!isXtreamStreamUrl(url) || !/^(mp4|m3u8|mkv|ts|avi)$/.test(currentExt)) {
            return [];
        }

        if (isXtreamLiveUrl(url)) {
            order = currentExt === "m3u8" ?
                ["ts", "mp4", "mkv", "avi"] :
                ["m3u8", "ts", "mp4", "mkv", "avi"];
        }

        for (i = 0; i < order.length; i += 1) {
            if (order[i] === currentExt) {
                continue;
            }
            candidate = makeSameUrlWithExtension(url, order[i]);
            if (candidate && candidate !== url) {
                fallbacks.push(candidate);
            }
        }

        candidate = makeUrlWithoutExtension(url);
        if (candidate && candidate !== url) {
            fallbacks.push(candidate);
        }

        return fallbacks;
    }

    function normalizeSafeUrl(url) {
        return String(url || "").trim();
    }

    function normalizeServerUrl(url) {
        return String(url || "").replace(/\/+$/, "");
    }

    function cloneItemWithUrl(item, url, typeOverride) {
        var cloned = {};
        var key;

        for (key in item) {
            if (Object.prototype.hasOwnProperty.call(item, key)) {
                cloned[key] = item[key];
            }
        }
        cloned.url = url;
        cloned.type = typeOverride || (/\/api\/stream_proxy\.php/i.test(url) ? item.type : (getUrlExtension(url) || item.type));
        return cloned;
    }

    function getVideoErrorDetails(video) {
        if (!video.error) {
            return "none";
        }

        return "code=" + video.error.code + ", message=" + (video.error.message || "n/a");
    }

    function mayBeCorsBlocked(error) {
        if (!error) {
            return false;
        }

        var details = String(error.details || error.message || error.type || "").toLowerCase();
        return details.indexOf("cors") !== -1 ||
            details.indexOf("network") !== -1 ||
            details.indexOf("manifest") !== -1 ||
            details.indexOf("frag") !== -1 ||
            details.indexOf("xhr") !== -1;
    }

    function mediaErrorMayBeCorsBlocked(video) {
        return video.error &&
            video.error.code === 2;
    }

    function stringifyLogDetails(details) {
        try {
            return JSON.stringify(details);
        } catch (error) {
            return String(details);
        }
    }

    function getTrackElementForTextTrack(video, textTrack) {
        var tracks;
        var i;

        if (!video || !textTrack) {
            return null;
        }

        tracks = video.querySelectorAll("track");
        for (i = 0; i < tracks.length; i += 1) {
            if (tracks[i].track === textTrack) {
                return tracks[i];
            }
        }

        return null;
    }

    function normalizeTrackLanguage(value) {
        var text = String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
        var map = {
            ar: "ar",
            ara: "ar",
            arabic: "ar",
            en: "en",
            eng: "en",
            english: "en",
            tr: "tr",
            tur: "tr",
            turkish: "tr",
            pt: "pt",
            por: "pt",
            portuguese: "pt",
            br: "pt",
            es: "es",
            spa: "es",
            spanish: "es",
            de: "de",
            ger: "de",
            deu: "de",
            german: "de",
            fr: "fr",
            fre: "fr",
            fra: "fr",
            french: "fr",
            hu: "hu",
            hun: "hu",
            hungarian: "hu",
            he: "he",
            heb: "he",
            hebrew: "he",
            cs: "cs",
            cze: "cs",
            ces: "cs",
            czech: "cs",
            fi: "fi",
            fin: "fi",
            finnish: "fi"
        };
        var parts = text.split(" ");
        var i;

        if (map[text]) {
            return map[text];
        }

        for (i = 0; i < parts.length; i += 1) {
            if (map[parts[i]]) {
                return map[parts[i]];
            }
        }

        return "";
    }

    function normalizeTrackLabel(value) {
        return String(value || "").toLowerCase().replace(/\s+/g, " ").trim();
    }

    function dedupeSubtitleTracks(tracks) {
        var seen = {};
        var result = [];

        tracks.forEach(function (track) {
            var key = normalizeTrackLanguage(track.language || track.label) || normalizeTrackLabel(track.label);
            var existingIndex;

            if (!key) {
                key = "track:" + result.length;
            }

            if (Object.prototype.hasOwnProperty.call(seen, key)) {
                existingIndex = seen[key];
                if (track.selected && !result[existingIndex].selected) {
                    result[existingIndex] = track;
                }
                return;
            }

            seen[key] = result.length;
            result.push(track);
        });

        return result;
    }

    function loadHlsScript(callback) {
        if (window.Hls) {
            callback(null);
            return;
        }

        hlsLoadCallbacks.push(callback);

        if (hlsLoadState === "loading") {
            return;
        }

        hlsLoadState = "loading";

        var script = document.createElement("script");
        script.src = HLS_JS_CDN;
        script.async = true;
        script.onload = function () {
            hlsLoadState = "loaded";
            flushHlsLoadCallbacks(null);
        };
        script.onerror = function () {
            var error = new Error("Could not load hls.js from " + HLS_JS_CDN);
            hlsLoadState = "failed";
            flushHlsLoadCallbacks(error);
        };

        document.head.appendChild(script);
    }

    function flushHlsLoadCallbacks(error) {
        var callbacks = hlsLoadCallbacks.slice();
        hlsLoadCallbacks = [];

        callbacks.forEach(function (callback) {
            callback(error);
        });
    }

    function loadMpegTsScript(callback) {
        if (window.mpegts) {
            callback(null);
            return;
        }

        mpegTsLoadCallbacks.push(callback);

        if (mpegTsLoadState === "loading") {
            return;
        }

        mpegTsLoadState = "loading";

        var script = document.createElement("script");
        script.src = MPEGTS_JS_CDN;
        script.async = true;
        script.onload = function () {
            mpegTsLoadState = "loaded";
            flushMpegTsLoadCallbacks(null);
        };
        script.onerror = function () {
            var error = new Error("Could not load mpegts.js from " + MPEGTS_JS_CDN);
            mpegTsLoadState = "failed";
            flushMpegTsLoadCallbacks(error);
        };

        document.head.appendChild(script);
    }

    function flushMpegTsLoadCallbacks(error) {
        var callbacks = mpegTsLoadCallbacks.slice();
        mpegTsLoadCallbacks = [];

        callbacks.forEach(function (callback) {
            callback(error);
        });
    }

    function TvVideoPlayer(options) {
        this.video = options.video;
        this.titleElement = options.titleElement;
        this.loadingElement = options.loadingElement;
        this.errorElement = options.errorElement;
        this.playPauseButton = options.playPauseButton;
        this.progressBar = options.progressBar;
        this.currentTimeElement = options.currentTimeElement;
        this.durationElement = options.durationElement;
        this.currentItem = null;
        this.hls = null;
        this.mpegts = null;
        this.mpegtsUrl = "";
        this.playbackStartTimer = null;
        this.liveRestartTimer = null;
        this.liveRestartAttempts = 0;
        this.liveRestartKey = "";
        this.hasPlaybackStarted = false;
        this.hasPlaybackFailed = false;
        this.loadMode = "";
        this.hlsFallbackTried = false;
        this.hlsRecoveryTried = false;
        this.mpegTsFallbackTried = false;
        this.playbackMode = "auto";
        this.serverUrl = options.serverUrl || window.NEXO_APP_SERVER_URL || "";
        this.streamProxyEndpoint = this.serverUrl ? normalizeServerUrl(this.serverUrl) + "/api/stream_proxy.php?url=" : "";
        this.fallbackUrls = [];
        this.fallbackIndex = 0;
        this.attemptedUrls = {};
        this.proxyFallbackUrls = {};
        this.avplayFallbackTried = false;
        this.avplay = null;
        this.avplayInterval = null;
        this.avplayDuration = 0;
        this.avplayCurrentTime = 0;
        this.avplayPaused = true;
        this.avplayTrackScanTimer = null;
        this.avplayTrackScanCount = 0;
        this.avplayTextTracks = [];
        this.avplayAudioTracks = [];
        this.avplayExternalSubtitleActive = false;
        this.manualSubtitleTextTrackIndex = -1;
        this.onTracksChanged = options.onTracksChanged || function () {};

        this.bindEvents();
    }

    TvVideoPlayer.prototype.bindEvents = function () {
        var self = this;

        this.video.addEventListener("loadstart", function () {
            self.showLoading(true);
            self.showError("");
            self.hasPlaybackFailed = false;
        });

        this.video.addEventListener("canplay", function () {
            self.clearPlaybackStartTimer();
            self.showLoading(false);
        });

        this.video.addEventListener("playing", function () {
            self.clearPlaybackStartTimer();
            self.hasPlaybackStarted = true;
            self.hasPlaybackFailed = false;
            self.showLoading(false);
            self.showError("");
            self.updatePlayState();
        });

        this.video.addEventListener("pause", function () {
            self.updatePlayState();
        });

        this.video.addEventListener("timeupdate", function () {
            self.updateProgress();
        });

        this.video.addEventListener("durationchange", function () {
            self.updateProgress();
        });

        this.video.addEventListener("loadedmetadata", function () {
            self.notifyTracksChanged();
        });

        this.video.addEventListener("waiting", function () {
            self.showLoading(true);
        });

        this.video.addEventListener("error", function () {
            self.handlePlaybackFailure("video element error");
        });

        this.video.addEventListener("ended", function () {
            self.handleStreamEnded("video ended");
        });

        window.addEventListener("resize", function () {
            self.updateAvplayDisplayRect();
        });
    };

    TvVideoPlayer.prototype.setPlaybackMode = function (mode) {
        this.playbackMode = mode || "auto";
    };

    TvVideoPlayer.prototype.notifyTracksChanged = function () {
        this.onTracksChanged();
    };

    TvVideoPlayer.prototype.canPlaySource = function (item) {
        if (isMpegTsSource(item)) {
            return canUseAvplay(item);
        }

        if (isMkvSource(item)) {
            return canUseAvplay(item);
        }

        if (isHlsSource(item)) {
            return true;
        }

        if (isMp4Source(item)) {
            return this.video.canPlayType("video/mp4");
        }

        return true;
    };

    TvVideoPlayer.prototype.load = function (item) {
        var url = normalizeSafeUrl(item && item.url);
        var playableItem = cloneItemWithUrl(item, url);

        if (this.liveRestartKey !== url) {
            this.liveRestartKey = url;
            this.liveRestartAttempts = 0;
        }
        this.currentItem = playableItem;
        this.titleElement.textContent = item.title;
        this.hasPlaybackStarted = false;
        this.hasPlaybackFailed = false;
        this.loadMode = "";
        this.hlsFallbackTried = false;
        this.hlsRecoveryTried = false;
        this.mpegTsFallbackTried = false;
        this.fallbackUrls = getXtreamExtensionFallbacks(url);
        this.fallbackIndex = 0;
        this.attemptedUrls = {};
        this.proxyFallbackUrls = {};
        this.avplayFallbackTried = false;
        this.manualSubtitleTextTrackIndex = -1;
        this.showError("");
        this.showLoading(true);
        this.updateProgress();
        this.clearPlaybackStartTimer();
        this.destroyHls();
        this.destroyMpegTs();
        this.destroyAvplay();
        this.loadSelectedEngine(playableItem, "initial");
    };

    TvVideoPlayer.prototype.loadSelectedEngine = function (item, reason) {
        this.currentItem = item;
        this.attemptedUrls[item.url] = true;
        this.startPlaybackStartTimer(item, reason);

        console.log("[playback] Attempt:", stringifyLogDetails({
            reason: reason,
            finalUrl: item.url,
            extension: getUrlExtension(item.url) || item.type || "unknown",
            tizenAvplay: !!canUseAvplay(item)
        }));

        if (isHlsSource(item)) {
            if (this.playbackMode === "auto" && canUseAvplay(item)) {
                this.loadAvplay(item);
                return;
            }
            if (this.playbackMode === "native") {
                this.loadHls(item);
                return;
            }
            this.loadWithHlsJs(item, reason || "HLS source");
            return;
        }

        if (isMkvSource(item) || isMpegTsSource(item)) {
            if (canUseAvplay(item)) {
                this.loadAvplay(item);
                return;
            }
            if (isMpegTsSource(item)) {
                this.loadWithMpegTsJs(item, reason || "MPEG-TS source");
                return;
            }
            var fallbackItem = this.getNextFallbackItem();
            if (fallbackItem) {
                this.loadSelectedEngine(fallbackItem, "browser unsupported extension fallback");
                return;
            }
            this.video.removeAttribute("src");
            this.video.load();
            this.showLoading(false);
            this.showError("Browser preview may fail for " + (getUrlExtension(item.url) || "this format").toUpperCase() + " because of CORS or unsupported codec. Test this stream on Tizen AVPlay.");
            console.log("[playback] Browser may fail due to CORS or unsupported codec. Test on Tizen AVPlay.", stringifyLogDetails({
                finalUrl: item.url,
                extension: getUrlExtension(item.url),
                selectedEngine: "none-browser-unsupported"
            }));
            return;
        }

        if (!this.canPlaySource(item)) {
            this.video.removeAttribute("src");
            this.video.load();
            this.showLoading(false);
            this.showError("This stream format is not supported by browser preview. Test it on Tizen AVPlay.");
            return;
        }

        this.loadNative(item.url);
    };

    TvVideoPlayer.prototype.applyAvplayExternalSubtitlePath = function (localPath, callback) {
        var self = this;

        if (!this.avplay || this.loadMode !== "avplay" || !localPath) {
            callback(false);
            return;
        }

        try {
            if (this.avplay.setSilentSubtitle) {
                this.avplay.setSilentSubtitle(false);
            }
            this.avplay.setExternalSubtitlePath(localPath);
            this.avplayExternalSubtitleActive = true;
            console.log("[subtitles] AVPlay external subtitle path:", localPath);
            setTimeout(function () {
                self.refreshAvplayTracks();
                self.notifyTracksChanged();
                callback(true);
            }, 400);
        } catch (error) {
            console.log("[subtitles] AVPlay setExternalSubtitlePath failed:", error.message || error);
            callback(false);
        }
    };

    TvVideoPlayer.prototype.downloadSubtitleForAvplay = function (subtitleUrl, callback) {
        var self = this;

        if (!subtitleUrl) {
            callback("");
            return;
        }

        if (window.tizen && window.tizen.download && window.tizen.DownloadRequest) {
            try {
                window.tizen.download.start(new window.tizen.DownloadRequest(subtitleUrl, "wgt-private-tmp"), {
                    oncompleted: function (downloadId, fullPath) {
                        console.log("[subtitles] Downloaded to:", fullPath);
                        callback(fullPath || "");
                    },
                    onfailed: function (error) {
                        console.log("[subtitles] Tizen download failed:", error);
                        callback("");
                    }
                });
                return;
            } catch (error) {
                console.log("[subtitles] Tizen download API error:", error.message || error);
            }
        }

        callback(subtitleUrl);
    };

    TvVideoPlayer.prototype.attachAvplayExternalSubtitle = function (preferred, allSubtitles, callback) {
        var self = this;
        var subtitle = preferred || (allSubtitles && allSubtitles[0]);

        if (!subtitle || !subtitle.url) {
            callback(false);
            return;
        }

        this.downloadSubtitleForAvplay(subtitle.url, function (localPath) {
            if (!localPath) {
                callback(false);
                return;
            }

            self.applyAvplayExternalSubtitlePath(localPath, callback);
        });
    };

    TvVideoPlayer.prototype.switchToHtml5WithSubtitles = function (item) {
        var resumeTime = this.getCurrentTime();

        console.log("[subtitles] Switching to HTML5 for external subtitles:", item.title);
        this.destroyAvplay();
        this.loadMode = "native";
        this.loadNative(item.url);

        if (resumeTime > 0) {
            var self = this;
            var applySeek = function () {
                if (!isFinite(self.video.duration) || self.video.duration <= 0) {
                    return;
                }
                self.seekTo(Math.max(0, resumeTime));
            };
            this.video.addEventListener("loadedmetadata", applySeek, { once: true });
            setTimeout(applySeek, 1200);
        }
    };

    TvVideoPlayer.prototype.loadAvplay = function (item) {
        var self = this;

        this.destroyAvplay();
        this.destroyMpegTs();
        this.loadMode = "avplay";
        this.avplay = webapis.avplay;
        this.avplayDuration = 0;
        this.avplayCurrentTime = 0;
        this.avplayPaused = true;
        this.avplayTrackScanCount = 0;
        this.avplayTextTracks = [];
        this.avplayAudioTracks = [];
        this.avplayExternalSubtitleActive = false;
        this.video.removeAttribute("src");
        this.video.load();

        console.log("[playback] Selected engine: AVPlay", stringifyLogDetails({
            finalUrl: item.url,
            extension: getUrlExtension(item.url) || item.type || "unknown"
        }));

        try {
            this.avplay.open(item.url);
            if (item.pendingAvplaySubtitlePath && this.avplay.setExternalSubtitlePath) {
                try {
                    this.avplay.setExternalSubtitlePath(item.pendingAvplaySubtitlePath);
                    item.pendingAvplaySubtitlePath = "";
                } catch (subtitleError) {
                    console.log("[subtitles] Pre-load external subtitle failed:", subtitleError.message || subtitleError);
                }
            }
            this.avplay.setListener({
                onbufferingstart: function () {
                    console.log("[playback] AVPlay buffering start");
                    self.showLoading(true);
                },
                onbufferingcomplete: function () {
                    console.log("[playback] AVPlay buffering complete");
                    self.showLoading(false);
                },
                onstreamcompleted: function () {
                    console.log("[playback] AVPlay stream completed");
                    if (self.handleStreamEnded("AVPlay stream completed")) {
                        return;
                    }
                    self.avplayPaused = true;
                    self.stopAvplayProgressTimer();
                    self.updatePlayState();
                },
                oncurrentplaytime: function (currentTime) {
                    self.avplayCurrentTime = currentTime || 0;
                    self.updateProgress();
                },
                onerror: function (eventType) {
                    console.log("[playback] AVPlay error:", eventType);
                    self.handlePlaybackFailure("AVPlay error", { message: eventType });
                },
                onsubtitlechange: function () {}
            });
            this.updateAvplayDisplayRect();
            this.avplay.prepareAsync(function () {
                self.clearPlaybackStartTimer();
                self.hasPlaybackStarted = true;
                self.hasPlaybackFailed = false;
                self.showLoading(false);
                self.showError("");
                self.updateAvplayDisplayRect();
                self.refreshAvplayTracks();
                self.scheduleAvplayTrackScans();
                try {
                    self.avplayDuration = self.avplay.getDuration();
                    self.avplay.play();
                    self.avplayPaused = false;
                    self.startAvplayProgressTimer();
                    self.updateProgress();
                } catch (playError) {
                    self.handlePlaybackFailure("AVPlay play failed", playError);
                }
            }, function (prepareError) {
                self.handlePlaybackFailure("AVPlay prepare failed", prepareError);
            });
        } catch (error) {
            this.handlePlaybackFailure("AVPlay load failed", error);
        }
    };

    TvVideoPlayer.prototype.updateAvplayDisplayRect = function () {
        var rect;

        if (!this.avplay || this.loadMode !== "avplay") {
            return;
        }

        try {
            rect = this.video.getBoundingClientRect();
            this.avplay.setDisplayRect(
                Math.round(rect.left),
                Math.round(rect.top),
                Math.round(rect.width),
                Math.round(rect.height)
            );
        } catch (error) {
            console.log("AVPlay display rect failed:", error.message || error);
        }
    };

    TvVideoPlayer.prototype.parseAvplayExtraInfo = function (track) {
        if (!track || !track.extra_info) {
            return {};
        }

        try {
            return JSON.parse(track.extra_info);
        } catch (error) {
            return {};
        }
    };

    TvVideoPlayer.prototype.refreshAvplayTracks = function () {
        var tracks;
        var i;
        var track;
        var extra;
        var label;
        var oldTextCount = this.avplayTextTracks.length;
        var oldAudioCount = this.avplayAudioTracks.length;

        this.avplayTextTracks = [];
        this.avplayAudioTracks = [];

        if (!this.avplay || this.loadMode !== "avplay") {
            return;
        }

        try {
            tracks = this.avplay.getTotalTrackInfo() || [];
            console.log("[subtitles] AVPlay raw tracks:", stringifyLogDetails(tracks));
        } catch (error) {
            console.log("[subtitles] AVPlay track scan failed:", error.message || error);
            tracks = [];
        }

        for (i = 0; i < tracks.length; i += 1) {
            track = tracks[i];
            extra = this.parseAvplayExtraInfo(track);
            label = extra.track_lang ||
                extra.language ||
                extra.lang ||
                track.language ||
                track.track_lang ||
                track.extra_info ||
                "";

            if (/^(TEXT|SUBTITLE)$/i.test(String(track.type || ""))) {
                this.avplayTextTracks.push({
                    id: parseInt(track.index, 10),
                    label: label || ("Subtitle " + (this.avplayTextTracks.length + 1)),
                    language: normalizeTrackLanguage(label),
                    selected: false
                });
            } else if (track.type === "AUDIO") {
                this.avplayAudioTracks.push({
                    id: parseInt(track.index, 10),
                    label: label || ("Audio " + (this.avplayAudioTracks.length + 1)),
                    selected: false
                });
            }
        }

        console.log("[subtitles] AVPlay text tracks:", this.avplayTextTracks.length);
        if (this.avplayTextTracks.length !== oldTextCount || this.avplayAudioTracks.length !== oldAudioCount) {
            this.notifyTracksChanged();
        }
    };

    TvVideoPlayer.prototype.scheduleAvplayTrackScans = function () {
        var self = this;

        this.stopAvplayTrackScans();
        this.avplayTrackScanCount = 0;

        this.avplayTrackScanTimer = window.setInterval(function () {
            self.avplayTrackScanCount += 1;
            self.refreshAvplayTracks();

            if (self.avplayTextTracks.length || self.avplayTrackScanCount >= 12) {
                self.stopAvplayTrackScans();
            }
        }, 1000);
    };

    TvVideoPlayer.prototype.stopAvplayTrackScans = function () {
        if (this.avplayTrackScanTimer) {
            window.clearInterval(this.avplayTrackScanTimer);
            this.avplayTrackScanTimer = null;
        }
    };

    TvVideoPlayer.prototype.startAvplayProgressTimer = function () {
        var self = this;

        this.stopAvplayProgressTimer();
        this.avplayInterval = window.setInterval(function () {
            self.updateProgress();
        }, 500);
    };

    TvVideoPlayer.prototype.stopAvplayProgressTimer = function () {
        if (this.avplayInterval) {
            window.clearInterval(this.avplayInterval);
            this.avplayInterval = null;
        }
    };

    TvVideoPlayer.prototype.loadNative = function (url) {
        this.loadMode = "native";
        this.destroyMpegTs();
        console.log("[playback] Selected engine: HTML5", stringifyLogDetails({
            finalUrl: url,
            extension: getUrlExtension(url) || "unknown"
        }));
        this.video.src = url;
        this.video.load();
        this.tryPlay();
    };

    TvVideoPlayer.prototype.loadHls = function (item) {
        var self = this;
        var nativeHlsType = this.video.canPlayType("application/vnd.apple.mpegurl") ||
            this.video.canPlayType("application/x-mpegURL");

        if (nativeHlsType || this.playbackMode === "native") {
            this.loadMode = "native-hls";
            console.log("[playback] Selected engine: native HLS", stringifyLogDetails({
                finalUrl: item.url,
                extension: getUrlExtension(item.url),
                support: nativeHlsType
            }));
            this.video.src = item.url;
            this.video.load();
            this.tryPlay();
            return;
        }

        this.loadWithHlsJs(item, "Native HLS unavailable");
    };

    TvVideoPlayer.prototype.loadWithHlsJs = function (item, reason) {
        var self = this;

        this.hlsFallbackTried = true;
        this.hlsRecoveryTried = false;
        this.loadMode = "hls.js";
        this.destroyHls();
        this.destroyMpegTs();
        this.video.removeAttribute("src");
        this.video.load();
        this.showLoading(true);
        this.showError("");

        console.log("[playback] Selected engine: HLS.js", stringifyLogDetails({
            reason: reason,
            finalUrl: item.url,
            extension: getUrlExtension(item.url),
            script: HLS_JS_CDN
        }));
        loadHlsScript(function (scriptError) {
            if (self.currentItem !== item) {
                return;
            }

            if (scriptError) {
                console.log("hls.js load error:", scriptError.message);
                self.handlePlaybackFailure("hls.js script load failed", scriptError);
                return;
            }

            if (!window.Hls || !window.Hls.isSupported()) {
                self.handlePlaybackFailure("hls.js is not supported in this browser");
                return;
            }

            self.attachHls(item.url);
        });
    };

    TvVideoPlayer.prototype.loadWithMpegTsJs = function (item, reason) {
        var self = this;

        this.mpegTsFallbackTried = true;
        this.loadMode = "mpegts.js";
        this.destroyHls();
        this.destroyMpegTs();
        this.video.removeAttribute("src");
        this.video.load();
        this.showLoading(true);
        this.showError("");

        console.log("[playback] Selected engine: mpegts.js", stringifyLogDetails({
            reason: reason,
            finalUrl: item.url,
            proxiedUrl: this.getStreamProxyUrl(item.url),
            extension: getUrlExtension(item.url),
            script: MPEGTS_JS_CDN
        }));

        loadMpegTsScript(function (scriptError) {
            if (self.currentItem !== item) {
                return;
            }

            if (scriptError) {
                console.log("mpegts.js load error:", scriptError.message);
                self.handlePlaybackFailure("mpegts.js script load failed", scriptError);
                return;
            }

            if (!window.mpegts || !window.mpegts.isSupported()) {
                self.handlePlaybackFailure("mpegts.js is not supported in this browser");
                return;
            }

            self.attachMpegTs(item.url);
        });
    };

    TvVideoPlayer.prototype.attachMpegTs = function (url) {
        var self = this;
        var playbackUrl = this.getStreamProxyUrl(url);

        this.mpegtsUrl = playbackUrl;
        this.mpegts = window.mpegts.createPlayer({
            type: "mpegts",
            isLive: true,
            url: playbackUrl
        }, {
            enableStashBuffer: false,
            liveBufferLatencyChasing: true
        });

        this.mpegts.on(window.mpegts.Events.ERROR, function (type, details, info) {
            console.log("mpegts.js error details:", stringifyLogDetails({
                src: playbackUrl,
                type: type,
                details: details,
                info: info
            }));
            self.handlePlaybackFailure("mpegts.js error", {
                type: type,
                details: details,
                message: info && (info.msg || info.message || info.code)
            });
        });

        this.mpegts.attachMediaElement(this.video);
        this.mpegts.load();
        this.tryPlay();
    };

    TvVideoPlayer.prototype.attachHls = function (url) {
        var self = this;

        console.log("[playback] Loading HLS.js source:", url);

        this.hls = this.createHlsInstance();
        if (this.hls) {
            this.hls.subtitleDisplay = true;
        }
        this.hls.on(window.Hls.Events.ERROR, function (event, data) {
            console.log("hls.js error details:", stringifyLogDetails({
                src: url,
                type: data && data.type,
                details: data && data.details,
                fatal: data && data.fatal,
                response: data && data.response
            }));

            if (data && data.fatal) {
                if (!self.hlsRecoveryTried && self.recoverHlsError(data)) {
                    return;
                }

                self.handlePlaybackFailure("hls.js fatal error", data);
            }
        });
        this.hls.on(window.Hls.Events.MANIFEST_PARSED, function () {
            self.notifyTracksChanged();
            self.tryPlay();
        });
        if (window.Hls.Events.AUDIO_TRACKS_UPDATED) {
            this.hls.on(window.Hls.Events.AUDIO_TRACKS_UPDATED, function () {
                self.notifyTracksChanged();
            });
        }
        if (window.Hls.Events.SUBTITLE_TRACKS_UPDATED) {
            this.hls.on(window.Hls.Events.SUBTITLE_TRACKS_UPDATED, function () {
                self.notifyTracksChanged();
            });
        }
        this.hls.loadSource(url);
        this.hls.attachMedia(this.video);
    };

    TvVideoPlayer.prototype.getAudioTracks = function () {
        var tracks = [];
        var i;

        if (this.loadMode === "avplay" && this.avplayAudioTracks.length) {
            return this.avplayAudioTracks.slice();
        }

        if (this.hls && this.hls.audioTracks && this.hls.audioTracks.length) {
            for (i = 0; i < this.hls.audioTracks.length; i += 1) {
                tracks.push({
                    id: i,
                    label: this.hls.audioTracks[i].name || this.hls.audioTracks[i].lang || ("Audio " + (i + 1)),
                    selected: this.hls.audioTrack === i
                });
            }
            return tracks;
        }

        if (this.video.audioTracks && this.video.audioTracks.length) {
            for (i = 0; i < this.video.audioTracks.length; i += 1) {
                tracks.push({
                    id: i,
                    label: this.video.audioTracks[i].label || this.video.audioTracks[i].language || ("Audio " + (i + 1)),
                    selected: !!this.video.audioTracks[i].enabled
                });
            }
        }

        return tracks;
    };

    TvVideoPlayer.prototype.setAudioTrack = function (index) {
        var i;

        if (this.loadMode === "avplay" && this.avplay) {
            try {
                if (index >= 0) {
                    this.avplay.setSelectTrack("AUDIO", index);
                }
                for (i = 0; i < this.avplayAudioTracks.length; i += 1) {
                    this.avplayAudioTracks[i].selected = index < 0 ? i === 0 : this.avplayAudioTracks[i].id === index;
                }
            } catch (error) {
                console.log("AVPlay audio select failed:", error.message || error);
            }
            return;
        }

        if (this.hls && this.hls.audioTracks && this.hls.audioTracks.length) {
            this.hls.audioTrack = index < 0 ? 0 : index;
            return;
        }

        if (this.video.audioTracks && this.video.audioTracks.length) {
            for (i = 0; i < this.video.audioTracks.length; i += 1) {
                this.video.audioTracks[i].enabled = index < 0 ? i === 0 : i === index;
            }
        }
    };

    TvVideoPlayer.prototype.getSubtitleTracks = function () {
        var tracks = [];
        var i;

        if (this.loadMode === "avplay") {
            if (!this.avplayTextTracks.length) {
                this.refreshAvplayTracks();
            }
            if (this.avplayTextTracks.length) {
                tracks = this.avplayTextTracks.slice();
            }
        }

        if (this.loadMode !== "avplay" && this.hls && this.hls.subtitleTracks && this.hls.subtitleTracks.length) {
            for (i = 0; i < this.hls.subtitleTracks.length; i += 1) {
                tracks.push({
                    id: i,
                    label: this.hls.subtitleTracks[i].name || this.hls.subtitleTracks[i].lang || ("Subtitle " + (i + 1)),
                    language: this.hls.subtitleTracks[i].lang || normalizeTrackLanguage(this.hls.subtitleTracks[i].name),
                    selected: this.hls.subtitleTrack === i
                });
            }
        }

        if (this.video.textTracks && this.video.textTracks.length) {
            console.log("[subtitles] HTML5 text tracks:", this.video.textTracks.length);
            for (i = 0; i < this.video.textTracks.length; i += 1) {
                var textTrack = this.video.textTracks[i];
                var trackElement = getTrackElementForTextTrack(this.video, textTrack);
                tracks.push({
                    id: this.loadMode === "avplay" ? HTML5_SUBTITLE_ID_OFFSET + i : tracks.length,
                    label: textTrack.label || textTrack.language || ("Subtitle " + (i + 1)),
                    language: textTrack.language || normalizeTrackLanguage(textTrack.label),
                    selected: textTrack.mode === "showing" ||
                        (!!(trackElement && trackElement.dataset && trackElement.dataset.cueKey) && i === this.manualSubtitleTextTrackIndex),
                    textTrackIndex: i,
                    cueKey: trackElement && trackElement.dataset ? (trackElement.dataset.cueKey || "") : ""
                });
            }
        }

        return dedupeSubtitleTracks(tracks);
    };

    TvVideoPlayer.prototype.setSubtitleTrack = function (index) {
        var i;
        var textTrackIndex;

        if (this.loadMode === "avplay" && this.avplay) {
            if (index >= HTML5_SUBTITLE_ID_OFFSET) {
                textTrackIndex = index - HTML5_SUBTITLE_ID_OFFSET;
                try {
                    if (this.avplay.setSilentSubtitle) {
                        this.avplay.setSilentSubtitle(true);
                    }
                } catch (silentError) {}
                for (i = 0; i < this.avplayTextTracks.length; i += 1) {
                    this.avplayTextTracks[i].selected = false;
                }
                if (this.video.textTracks && this.video.textTracks.length) {
                    for (i = 0; i < this.video.textTracks.length; i += 1) {
                        this.video.textTracks[i].mode = i === textTrackIndex ? "hidden" : "disabled";
                    }
                }
                this.manualSubtitleTextTrackIndex = textTrackIndex;
                return;
            }

            try {
                if (index >= 0) {
                    if (this.avplay.setSilentSubtitle) {
                        this.avplay.setSilentSubtitle(false);
                    }
                    this.avplay.setSelectTrack("TEXT", index);
                    console.log("[subtitles] AVPlay selected text track:", index);
                } else if (this.avplay.setSilentSubtitle) {
                    this.avplay.setSilentSubtitle(true);
                    console.log("[subtitles] AVPlay subtitles disabled");
                }
                for (i = 0; i < this.avplayTextTracks.length; i += 1) {
                    this.avplayTextTracks[i].selected = index >= 0 && this.avplayTextTracks[i].id === index;
                }
                this.manualSubtitleTextTrackIndex = -1;
                if (this.video.textTracks && this.video.textTracks.length) {
                    for (i = 0; i < this.video.textTracks.length; i += 1) {
                        this.video.textTracks[i].mode = "disabled";
                    }
                }
            } catch (error) {
                console.log("[subtitles] AVPlay subtitle select failed:", error.message || error);
            }
            return;
        }

        if (this.hls && this.hls.subtitleTracks && this.hls.subtitleTracks.length) {
            if (index < this.hls.subtitleTracks.length) {
                this.hls.subtitleDisplay = index >= 0;
                this.hls.subtitleTrack = index;
                this.manualSubtitleTextTrackIndex = -1;
                if (this.video.textTracks && this.video.textTracks.length) {
                    for (i = 0; i < this.video.textTracks.length; i += 1) {
                        this.video.textTracks[i].mode = "disabled";
                    }
                }
                return;
            }
            index -= this.hls.subtitleTracks.length;
            this.hls.subtitleDisplay = false;
            this.hls.subtitleTrack = -1;
        }

        if (this.video.textTracks && this.video.textTracks.length) {
            for (i = 0; i < this.video.textTracks.length; i += 1) {
                var trackElement = getTrackElementForTextTrack(this.video, this.video.textTracks[i]);
                var isSelected = i === index;
                var usesManualOverlay = !!(trackElement && trackElement.dataset && trackElement.dataset.cueKey);

                this.video.textTracks[i].mode = isSelected ? (usesManualOverlay ? "hidden" : "showing") : "disabled";
                if (isSelected && usesManualOverlay) {
                    this.manualSubtitleTextTrackIndex = i;
                } else if (isSelected) {
                    this.manualSubtitleTextTrackIndex = -1;
                }
            }
            if (index < 0) {
                this.manualSubtitleTextTrackIndex = -1;
            }
        }
    };

    TvVideoPlayer.prototype.recoverHlsError = function (data) {
        if (!this.hls || !window.Hls) {
            return false;
        }

        this.hlsRecoveryTried = true;

        if (data.type === window.Hls.ErrorTypes.NETWORK_ERROR) {
            console.log("hls.js recovery: retrying network load");
            this.hls.startLoad();
            return true;
        }

        if (data.type === window.Hls.ErrorTypes.MEDIA_ERROR) {
            console.log("hls.js recovery: recovering media error");
            this.hls.recoverMediaError();
            return true;
        }

        return false;
    };

    TvVideoPlayer.prototype.tryPlay = function () {
        var self = this;

        var playPromise = this.video.play();
        if (playPromise && typeof playPromise.catch === "function") {
            playPromise.catch(function (error) {
                // Some environments require OK/Play before playback starts.
                console.log("video.play() was blocked or failed:", stringifyLogDetails({
                    src: self.getCurrentSrc(),
                    error: error && error.message,
                    videoError: getVideoErrorDetails(self.video)
                }));
            });
        }
    };

    TvVideoPlayer.prototype.togglePlay = function () {
        if (this.hasPlaybackFailed && this.currentItem) {
            this.load(this.currentItem);
            return;
        }

        if (this.loadMode === "avplay" && this.avplay) {
            try {
                if (this.avplayPaused) {
                    this.avplay.play();
                    this.avplayPaused = false;
                    this.startAvplayProgressTimer();
                } else {
                    this.avplay.pause();
                    this.avplayPaused = true;
                    this.stopAvplayProgressTimer();
                }
                this.updatePlayState();
            } catch (error) {
                this.handlePlaybackFailure("AVPlay toggle failed", error);
            }
            return;
        }

        if (!this.video.src && !this.hls && !this.mpegts) {
            if (this.currentItem) {
                this.load(this.currentItem);
            }
            return;
        }

        if (this.video.paused) {
            this.tryPlay();
        } else {
            this.video.pause();
        }
    };

    TvVideoPlayer.prototype.seekBy = function (seconds) {
        var targetMs;
        if (this.loadMode === "avplay" && this.avplay) {
            try {
                targetMs = Math.max(0, Math.min(
                    this.getDuration() * 1000,
                    (this.getCurrentTime() * 1000) + (seconds * 1000)
                ));
                this.avplay.seekTo(targetMs);
                this.avplayCurrentTime = targetMs;
                this.updateProgress();
            } catch (error) {
                console.log("AVPlay seek failed:", error.message || error);
            }
            return;
        }

        if (!isFinite(this.video.duration)) {
            return;
        }

        var target = Math.max(0, Math.min(this.video.duration, this.video.currentTime + seconds));
        this.video.currentTime = target;
        this.updateProgress();
    };

    TvVideoPlayer.prototype.seekTo = function (seconds) {
        var targetMs;

        if (this.loadMode === "avplay" && this.avplay) {
            try {
                targetMs = Math.max(0, Math.min(this.getDuration() * 1000, seconds * 1000));
                this.avplay.seekTo(targetMs);
                this.avplayCurrentTime = targetMs;
                this.updateProgress();
            } catch (error) {
                console.log("AVPlay seek failed:", error.message || error);
            }
            return;
        }

        if (isFinite(this.video.duration) && this.video.duration > 0) {
            this.video.currentTime = Math.min(seconds, Math.max(0, this.video.duration - 10));
            this.updateProgress();
        }
    };

    TvVideoPlayer.prototype.stop = function () {
        this.video.pause();
        this.clearPlaybackStartTimer();
        if (this.liveRestartTimer) {
            window.clearTimeout(this.liveRestartTimer);
            this.liveRestartTimer = null;
        }
        this.currentItem = null;
        this.destroyHls();
        this.destroyMpegTs();
        this.destroyAvplay();
        this.video.removeAttribute("src");
        this.video.load();
        this.hasPlaybackStarted = false;
        this.hasPlaybackFailed = false;
        this.hlsRecoveryTried = false;
        this.mpegTsFallbackTried = false;
        this.liveRestartAttempts = 0;
        this.liveRestartKey = "";
        this.manualSubtitleTextTrackIndex = -1;
        this.showLoading(false);
        this.showError("");
        this.updateProgress();
    };

    TvVideoPlayer.prototype.startPlaybackStartTimer = function (item, reason) {
        var self = this;

        this.clearPlaybackStartTimer();
        this.playbackStartTimer = window.setTimeout(function () {
            if (self.currentItem === item && !self.hasPlaybackStarted && !self.hasPlaybackFailed) {
                console.log("[playback] Startup timeout:", stringifyLogDetails({
                    reason: reason,
                    mode: self.loadMode,
                    url: item && item.url
                }));
                self.handlePlaybackFailure("playback startup timed out", {
                    type: self.loadMode,
                    message: "No playable media after startup timeout"
                });
            }
        }, 22000);
    };

    TvVideoPlayer.prototype.clearPlaybackStartTimer = function () {
        if (this.playbackStartTimer) {
            window.clearTimeout(this.playbackStartTimer);
            this.playbackStartTimer = null;
        }
    };

    TvVideoPlayer.prototype.destroyHls = function () {
        if (this.hls) {
            this.hls.destroy();
            this.hls = null;
        }
    };

    TvVideoPlayer.prototype.destroyMpegTs = function () {
        if (!this.mpegts) {
            this.mpegtsUrl = "";
            return;
        }

        try {
            this.mpegts.unload();
        } catch (unloadError) {}

        try {
            this.mpegts.detachMediaElement();
        } catch (detachError) {}

        try {
            this.mpegts.destroy();
        } catch (destroyError) {}

        this.mpegts = null;
        this.mpegtsUrl = "";
    };

    TvVideoPlayer.prototype.destroyAvplay = function () {
        if (!this.avplay) {
            return;
        }

        this.stopAvplayProgressTimer();
        this.stopAvplayTrackScans();

        try {
            this.avplay.stop();
        } catch (stopError) {}

        try {
            this.avplay.close();
        } catch (closeError) {}

        this.avplay = null;
        this.avplayDuration = 0;
        this.avplayCurrentTime = 0;
        this.avplayPaused = true;
        this.avplayTrackScanCount = 0;
        this.avplayTextTracks = [];
        this.avplayAudioTracks = [];
        this.avplayExternalSubtitleActive = false;
        this.manualSubtitleTextTrackIndex = -1;
    };

    TvVideoPlayer.prototype.isPaused = function () {
        if (this.loadMode === "avplay") {
            return this.avplayPaused;
        }

        return this.video.paused;
    };

    TvVideoPlayer.prototype.getCurrentTime = function () {
        if (this.loadMode === "avplay" && this.avplay) {
            try {
                this.avplayCurrentTime = this.avplay.getCurrentTime();
            } catch (error) {}
            return (this.avplayCurrentTime || 0) / 1000;
        }

        return this.video.currentTime || 0;
    };

    TvVideoPlayer.prototype.getDuration = function () {
        if (this.loadMode === "avplay" && this.avplay) {
            try {
                this.avplayDuration = this.avplay.getDuration();
            } catch (error) {}
            return (this.avplayDuration || 0) / 1000;
        }

        return this.video.duration || 0;
    };

    TvVideoPlayer.prototype.getCurrentSrc = function () {
        return this.video.currentSrc || this.video.src || this.mpegtsUrl || (this.currentItem && this.currentItem.url) || "";
    };

    TvVideoPlayer.prototype.getNextFallbackItem = function () {
        var url;

        while (this.fallbackIndex < this.fallbackUrls.length) {
            url = normalizeSafeUrl(this.fallbackUrls[this.fallbackIndex]);
            this.fallbackIndex += 1;
            if (url && !this.attemptedUrls[url]) {
                console.log("[playback] Fallback attempt URL:", url);
                return cloneItemWithUrl(this.currentItem, url);
            }
        }

        return null;
    };

    TvVideoPlayer.prototype.canUseStreamProxy = function (item) {
        return !!(this.streamProxyEndpoint &&
            item &&
            item.url &&
            /^https?:\/\//i.test(item.url) &&
            !/\/api\/stream_proxy\.php/i.test(item.url) &&
            !isHlsSource(item) &&
            (isMp4Source(item) || isXtreamVodUrl(item.url)));
    };

    TvVideoPlayer.prototype.getProxyFallbackItem = function (item) {
        var originalUrl = item && item.url;
        var proxyUrl;

        if (!this.canUseStreamProxy(item) || this.proxyFallbackUrls[originalUrl]) {
            return null;
        }

        this.proxyFallbackUrls[originalUrl] = true;
        proxyUrl = this.streamProxyEndpoint + encodeURIComponent(originalUrl);
        console.log("[playback] Proxy fallback URL:", proxyUrl);
        return cloneItemWithUrl(item, proxyUrl, item.type || getUrlExtension(originalUrl));
    };

    TvVideoPlayer.prototype.getStreamProxyUrl = function (url) {
        if (!this.streamProxyEndpoint ||
                !/^https?:\/\//i.test(url || "") ||
                /\/api\/stream_proxy\.php/i.test(url || "")) {
            return url;
        }

        return this.streamProxyEndpoint + encodeURIComponent(url);
    };

    TvVideoPlayer.prototype.createHlsInstance = function () {
        return new window.Hls({
            liveDurationInfinity: true,
            liveSyncDurationCount: 3,
            maxBufferLength: 30,
            backBufferLength: 20,
            manifestLoadingMaxRetry: 6,
            levelLoadingMaxRetry: 6,
            fragLoadingMaxRetry: 6,
            manifestLoadingRetryDelay: 1000,
            levelLoadingRetryDelay: 1000,
            fragLoadingRetryDelay: 1000
        });
    };

    TvVideoPlayer.prototype.handleStreamEnded = function (reason) {
        var self = this;
        var item = this.currentItem;

        if (!isLivePlaybackItem(item)) {
            return false;
        }

        if (this.liveRestartTimer) {
            return true;
        }

        this.liveRestartAttempts += 1;
        console.log("[playback] Live stream ended; reopening source:", stringifyLogDetails({
            reason: reason,
            attempt: this.liveRestartAttempts,
            url: item && item.url
        }));

        this.showLoading(true);
        this.showError("");
        this.liveRestartTimer = window.setTimeout(function () {
            var reloadItem = item;

            self.liveRestartTimer = null;
            if (self.currentItem !== reloadItem) {
                return;
            }

            self.destroyHls();
            self.destroyMpegTs();
            self.destroyAvplay();
            self.video.removeAttribute("src");
            self.video.load();
            self.hasPlaybackStarted = false;
            self.hasPlaybackFailed = false;
            self.hlsRecoveryTried = false;
            self.mpegTsFallbackTried = false;
            self.avplayFallbackTried = false;
            self.loadSelectedEngine(reloadItem, "live stream refresh after end");
        }, Math.min(6000, 900 + (this.liveRestartAttempts * 350)));

        return true;
    };

    TvVideoPlayer.prototype.handlePlaybackFailure = function (reason, error) {
        this.clearPlaybackStartTimer();
        var src = this.getCurrentSrc();
        var corsWarning = (mayBeCorsBlocked(error) || mediaErrorMayBeCorsBlocked(this.video)) ?
            " The stream may be blocked by CORS or network access rules." :
            "";
        var fallbackItem;
        var proxyItem;

        console.log("[playback] Playback error:", stringifyLogDetails({
            reason: reason,
            src: src,
            currentUrl: this.currentItem && this.currentItem.url,
            extension: this.currentItem ? getUrlExtension(this.currentItem.url) : "",
            selectedEngine: this.loadMode || "unknown",
            videoError: getVideoErrorDetails(this.video),
            avplayError: this.loadMode === "avplay" ? (error && (error.message || error.type || error.details || error)) : null,
            hlsError: error ? {
                type: error.type,
                details: error.details,
                fatal: error.fatal,
                message: error.message,
                response: error.response
            } : null
        }));

        if (!this.hasPlaybackStarted && this.loadMode === "avplay" && isMpegTsSource(this.currentItem) && !this.mpegTsFallbackTried) {
            console.log("[playback] AVPlay failed before start; retrying same MPEG-TS URL with mpegts.js:", this.currentItem.url);
            this.destroyAvplay();
            this.loadWithMpegTsJs(this.currentItem, "AVPlay failed before start");
            return;
        }

        if (!this.hasPlaybackStarted && this.loadMode === "avplay" && isHlsSource(this.currentItem) && !this.hlsFallbackTried) {
            console.log("[playback] AVPlay failed before start; retrying same HLS URL with hls.js:", this.currentItem.url);
            this.destroyAvplay();
            this.loadWithHlsJs(this.currentItem, "AVPlay failed before start");
            return;
        }

        if (!this.hasPlaybackStarted && this.loadMode !== "avplay" && canUseAvplay(this.currentItem) && !this.avplayFallbackTried) {
            console.log("[playback] HTML5 failed before start; retrying same URL with AVPlay:", this.currentItem.url);
            this.avplayFallbackTried = true;
            this.destroyHls();
            this.destroyMpegTs();
            this.loadAvplay(this.currentItem);
            return;
        }

        fallbackItem = !this.hasPlaybackStarted ? this.getNextFallbackItem() : null;
        if (fallbackItem) {
            this.destroyHls();
            this.destroyMpegTs();
            this.destroyAvplay();
            this.loadSelectedEngine(fallbackItem, "extension fallback after " + reason);
            return;
        }

        proxyItem = !this.hasPlaybackStarted && this.loadMode !== "avplay" ? this.getProxyFallbackItem(this.currentItem) : null;
        if (proxyItem) {
            this.destroyHls();
            this.destroyMpegTs();
            this.destroyAvplay();
            this.loadSelectedEngine(proxyItem, "stream proxy fallback after " + reason);
            return;
        }

        this.hasPlaybackFailed = true;
        this.showLoading(false);

        this.showError("This video could not be played. The emulator/browser may not support this live stream codec, or the stream server blocked access." + corsWarning);
    };

    TvVideoPlayer.prototype.updatePlayState = function () {
        if (this.loadMode === "avplay") {
            this.playPauseButton.textContent = this.avplayPaused ? "Play" : "Pause";
            return;
        }

        this.playPauseButton.textContent = this.video.paused ? "Play" : "Pause";
    };

    TvVideoPlayer.prototype.updateProgress = function () {
        var duration = this.getDuration();
        var currentTime = this.getCurrentTime();
        var progress = isFinite(duration) && duration > 0 ? (currentTime / duration) * 100 : 0;

        this.progressBar.style.width = progress + "%";
        this.currentTimeElement.textContent = formatTime(currentTime);
        this.durationElement.textContent = formatTime(duration);
        this.updatePlayState();
    };

    TvVideoPlayer.prototype.showLoading = function (isVisible) {
        this.loadingElement.hidden = !isVisible;
    };

    TvVideoPlayer.prototype.showError = function (message) {
        this.errorElement.textContent = message;
        this.errorElement.hidden = !message;
    };

    window.TvVideoPlayer = TvVideoPlayer;
})();
