zhw2590582 / ArtPlayer

:art: ArtPlayer.js is a modern and full featured HTML5 video player
https://artplayer.org
MIT License
2.64k stars 275 forks source link

Multi Audio supporting #851

Closed itzzzme closed 3 weeks ago

itzzzme commented 1 month ago

Currently is there any option that the player supports multiple audios along with multiple qualities of the hls stream?

zhw2590582 commented 1 month ago

Are you looking for:artplayer-plugin-hls-control

itzzzme commented 4 weeks ago

Thanks @zhw2590582! That's exactly what I am looking for

itzzzme commented 4 weeks ago

@zhw2590582 I had two questions

  1. How to add plugins once the player has already been initiated because once the player has been initiated in order to add a plugin or do any kind of change on pressing button(not associated with player) it doesn't reflect any change.I have to reload the player which is not a viable option.

  2. how to trigger function on video ending. I have tried video:ended from the official docs but it didn't work

zhw2590582 commented 4 weeks ago

You can add the plug-in after the player initialization is completed, and then I tested that the video:ended event was triggered normally. demo%3B%0A%7D)%3B%0A%0Aart.on(%27video%3Aended%27%2C%20()%20%3D%3E%20%7B%0A%09console.log(%27----%3E%27%2C%20%27video%3Aended%27)%3B%0A%7D)%3B)

itzzzme commented 4 weeks ago

in demo what you have done in this part

art.on('ready', () => {
    art.plugins.add(function yourPlugin(art) {
        console.log('yourPlugin');
    });
});

currently I am doing exactly this but what I am saying is on changing of some variable if you try to add some plugins it doesn't work

my implementation

image

but in this code problem is after initialization of player if autoSkipInto variable changes it doesn't affect the plugins adding

scenario: initaily the autoSkipIntro was false and player has initialized but in between of playing video if it gets true the it doesn't do that plugins adding portion

itzzzme commented 4 weeks ago

image

getting this error when adding this lines

art.on("video:ended", () => {
      console.log("---->", "video:ended");
    });

my full code

import Hls from "hls.js";
import { useEffect, useRef } from "react";
import Artplayer from "artplayer";
import artplayerPluginHlsQuality from "artplayer-plugin-hls-quality";
import artplayerPluginChapter from "./artPlayerPluinChaper";
import artplayerPluginSkip from "./autoSkip";
import artplayerPluginVttThumbnail from "./artPlayerPluginVttThumbnail";
import {
  backward10Icon,
  captionIcon,
  forward10Icon,
  fullScreenOffIcon,
  fullScreenOnIcon,
  loadingIcon,
  logo,
  muteIcon,
  pauseIcon,
  pipIcon,
  playIcon,
  playIconLg,
  settingsIcon,
  volumeIcon,
} from "./PlayerStyle";
import "./Player.css";
import website_name from "@/src/config/website";
import getChapterStyles from "./getChapterStyle";

const KEY_CODES = {
  M: "m",
  I: "i",
  F: "f",
  V: "v",
  SPACE: " ",
  ARROW_UP: "arrowup",
  ARROW_DOWN: "arrowdown",
  ARROW_RIGHT: "arrowright",
  ARROW_LEFT: "arrowleft",
};

export default function Player({
  streamUrl,
  subtitles,
  thumbnail,
  intro,
  outro,
  autoSkipIntro,
  autoPlay,
}) {
  const artRef = useRef(null);
  const proxy = import.meta.env.VITE_PROXY_URL;
  const m3u8proxy = import.meta.env.VITE_M3U8_PROXY_URL;

  useEffect(() => {
    const applyChapterStyles = () => {
      const existingStyles = document.querySelectorAll(
        "style[data-chapter-styles]"
      );
      existingStyles.forEach((style) => style.remove());
      const styleElement = document.createElement("style");
      styleElement.setAttribute("data-chapter-styles", "true");
      const styles = getChapterStyles(intro, outro);
      styleElement.textContent = styles;
      document.head.appendChild(styleElement);
      return () => {
        styleElement.remove();
      };
    };

    if (streamUrl || intro || outro) {
      const cleanup = applyChapterStyles();
      return cleanup;
    }
  }, [streamUrl, intro, outro]);

  const playM3u8 = (video, url, art) => {
    if (Hls.isSupported()) {
      if (art.hls) art.hls.destroy();
      const hls = new Hls();
      hls.loadSource(url);
      hls.attachMedia(video);
      art.hls = hls;

      art.on("destroy", () => hls.destroy());
      hls.on(Hls.Events.ERROR, (event, data) => {
        console.error("HLS.js error:", data);
      });
    } else if (video.canPlayType("application/vnd.apple.mpegurl")) {
      video.src = url;
    } else {
      art.notice.show("Unsupported playback format: m3u8");
    }
  };

  const createChapters = () => {
    const chapters = [];
    if (intro?.start !== 0 || intro?.end !== 0) {
      chapters.push({ start: intro.start, end: intro.end, title: "intro" });
    }
    if (outro?.start !== 0 || outro?.end !== 0) {
      chapters.push({ start: outro.start, end: outro.end, title: "outro" });
    }
    return chapters;
  };

  const handleKeydown = (event, art) => {
    const tagName = event.target.tagName.toLowerCase();

    if (tagName === "input" || tagName === "textarea") return;

    switch (event.key.toLowerCase()) {
      case KEY_CODES.M:
        art.muted = !art.muted;
        break;
      case KEY_CODES.I:
        art.pip = !art.pip;
        break;
      case KEY_CODES.F:
        event.preventDefault();
        event.stopPropagation();
        art.fullscreen = !art.fullscreen;
        break;
      case KEY_CODES.V:
        event.preventDefault();
        event.stopPropagation();
        art.subtitle.show = !art.subtitle.show;
        break;
      case KEY_CODES.SPACE:
        event.preventDefault();
        event.stopPropagation();
        art.playing ? art.pause() : art.play();
        break;
      case KEY_CODES.ARROW_UP:
        event.preventDefault();
        event.stopPropagation();
        art.volume = Math.min(art.volume + 0.1, 1);
        break;
      case KEY_CODES.ARROW_DOWN:
        event.preventDefault();
        event.stopPropagation();
        art.volume = Math.max(art.volume - 0.1, 0);
        break;
      case KEY_CODES.ARROW_RIGHT:
        event.preventDefault();
        event.stopPropagation();
        art.currentTime = Math.min(art.currentTime + 10, art.duration);
        break;
      case KEY_CODES.ARROW_LEFT:
        event.preventDefault();
        event.stopPropagation();
        art.currentTime = Math.max(art.currentTime - 10, 0);
        break;
      default:
        break;
    }
  };

  useEffect(() => {
    if (!streamUrl || !artRef.current) return;
    Artplayer.CONTEXTMENU = false;
    const art = new Artplayer({
      url: m3u8proxy + streamUrl,
      container: artRef.current,
      type: "m3u8",
      volume: 1,
      setting: true,
      playbackRate: true,
      pip: true,
      hotkey: false,
      fullscreen: true,
      mutex: true,
      playsInline: true,
      autoPlayback: true,
      lock: true,
      airplay: true,
      autoOrientation: true,
      fastForward: true,
      autoplay: autoPlay,
      plugins: [
        artplayerPluginHlsQuality({
          setting: true,
          getResolution: (level) => level.height + "P",
          title: "Quality",
          auto: "Auto",
        }),
        artplayerPluginChapter({ chapters: createChapters() }),
      ],
      subtitle: {
        style: {
          "font-weight": "400",
          "background-color": "rgba(0, 0, 0, 0.65)",
          height: "fit-content",
          width: "fit-content",
          marginInline: "auto",
          "margin-top": "auto",
          "margin-bottom": "2rem",
          left: "50%",
          transform: "translateX(-50%)",
          color: "#fff",
        },
        escape: false,
      },
      layers: [
        {
          name: website_name,
          html: logo,
          tooltip: website_name,
          style: {
            opacity: 1,
            position: "absolute",
            top: "5px",
            right: "5px",
            transition: "opacity 0.5s ease-out",
          },
        },
      ],
      controls: [
        {
          html: backward10Icon,
          position: "right",
          tooltip: "Backward 10s",
          click: () => {
            art.currentTime = Math.max(art.currentTime - 10, 0);
          },
        },
        {
          html: forward10Icon,
          position: "right",
          tooltip: "Forward 10s",
          click: () => {
            art.currentTime = Math.min(art.currentTime + 10, art.duration);
          },
        },
      ],
      icons: {
        play: playIcon,
        pause: pauseIcon,
        setting: settingsIcon,
        volume: volumeIcon,
        pip: pipIcon,
        volumeClose: muteIcon,
        state: playIconLg,
        loading: loadingIcon,
        fullscreenOn: fullScreenOnIcon,
        fullscreenOff: fullScreenOffIcon,
      },
      customType: {
        m3u8: playM3u8,
      },
    });
    art.on("resize", () => {
      art.subtitle.style({
        fontSize:
          (art.height > 500 ? art.height * 0.02 : art.height * 0.04) + "px",
      });
    });

    art.on("ready", () => {
      document.addEventListener("keydown", (event) =>
        handleKeydown(event, art)
      );
      art.subtitle.style({
        fontSize:
          (art.height > 500 ? art.height * 0.02 : art.height * 0.04) + "px",
      });
      thumbnail &&
        art.plugins.add(
          artplayerPluginVttThumbnail({
            vtt: `${proxy}${thumbnail}`,
          })
        );
      subtitles &&
        subtitles.length > 0 &&
        art.setting.add({
          name: "captions",
          icon: captionIcon,
          html: "Subtitle",
          tooltip:
            subtitles.find((sub) => sub.label.toLowerCase() === "english")
              ?.label || "default",
          position: "right",
          selector: [
            {
              html: "Display",
              switch: true,
              onSwitch: function (item) {
                item.tooltip = item.switch ? "Hide" : "Show";
                art.subtitle.show = !item.switch;
                return !item.switch;
              },
            },
            ...subtitles.map((sub) => ({
              default: sub.label === "English",
              html: sub.label,
              url: sub.file,
            })),
          ],
          onSelect: function (item) {
            art.subtitle.switch(item.url, { name: item.html });
            return item.html;
          },
        });
      console.log(autoSkipIntro);
      autoSkipIntro &&
        art.plugins.add(
          artplayerPluginSkip([
            ...(intro.start && intro.end ? [[intro.start, intro.end]] : []),
            ...(outro.start && outro.end ? [[outro.start, outro.end]] : []),
          ])
        );
      setTimeout(() => {
        art.layers[website_name].style.opacity = 0;
      }, 2000);
    });

    const defaultSubtitle = subtitles?.find((sub) => sub.label === "English");
    if (defaultSubtitle) {
      art.subtitle.switch(defaultSubtitle.file, {
        name: defaultSubtitle.label,
      });
    }
    art.on("video:ended", () => {
      console.log("---->", "video:ended");
    });
    return () => {
      if (art && art.destroy) {
        art.destroy(false);
      }
    };
  }, [streamUrl, subtitles, intro, outro, autoSkipIntro]);

  return <div ref={artRef} className="w-full h-full"></div>;
}
itzzzme commented 4 weeks ago

I have tested the m3u8 link with several other option and also with standalone hls.js it's not giving any error. But this error is only coming in artplayer when tyring some events (i.e: video:ended in this case)

here is the comparison

with artplayer it can't read some packets

https://github.com/user-attachments/assets/2283f7f5-dbd8-45de-8bb2-75f9be455bc1

same link with normal hls.js standalone library

https://github.com/user-attachments/assets/4409885c-f79a-4d2a-963a-baab5c902e67

it will be really helpful if you can tell the issue here

itzzzme commented 3 weeks ago

btw can i do something like this to rewind in mobile on double tap by adding layers

{
 html: "",
 style: {
            position: "absolute",
            left: 0,
            top: 0,
            width: "40%",
            height: "100%",
  },
  disable: !Artplayer.utils.isMobile,
   dblclick: function () {
            art.currentTime = Math.max(0, art.currentTime - 10);
   }
},
zhw2590582 commented 3 weeks ago

I don't quite understand your logic, and your code is a bit complicated, but the error about the video is from the decoding of hls

itzzzme commented 3 weeks ago

I don't quite understand your logic, and your code is a bit complicated, but the error about the video is from the decoding of hls

Understood. Thanks for your help