chintan9 / plyr-react

A simple, accessible and customisable react media player for Video, Audio, YouTube and Vimeo
https://github.com/chintan9/plyr-react
MIT License
485 stars 54 forks source link

Custom Controls on iOS #938

Open c-mella opened 2 years ago

c-mella commented 2 years ago

I am using usePlyr to create a custom plyr instance but on mobile the audio controls don't work. The icon shows as unmuted and doesn't unmute the audio when tapped. Has anyone else had this issue?

import React, { useState, useContext } from "react";

import type { APITypes } from "plyr-react";

import { usePlyr, PlyrProps, PlyrInstance } from "plyr-react";

import styles from "./Play.module.scss";

import { CoreContext } from "~context";
import hexToRgb from "~shared/utils/hexToRgb";

import { useGTM } from "~hooks/useGTM";

const CustomPlyr = React.forwardRef<APITypes, PlyrProps>((props, ref: any) => {
    const { playerStyle, size }: any = useContext(CoreContext);

    const { track } = useGTM();

    const { source, options = null } = props;

    const raptorRef = usePlyr(ref, { options, source });

    const { poster } = source || {};
    const { active } = options?.loop || {};
    const { autoplay } = options || {};

    const { gradient, subtitles, ui, full } = playerStyle;

    const [current, setCurrent] = useState(0);
    const [progress, setProgress] = useState(0);
    const [playing, setPlaying] = useState(autoplay);
    const [muted, setMuted] = useState(true);
    const [showLoopScreen, setShowLoop] = useState(false);
    const [counter, setCounter] = useState<number>(0);

    React.useEffect(() => {
        if (ref?.current?.plyr?.source === null) return;
        const api = ref?.current as { plyr: PlyrInstance };

        setMuted(api?.plyr?.volume === 0);

        const canPlay = () => {
            if (autoplay && !playing) {
                api?.plyr?.play();
            }
        };

        const onPlay = () => {
            setShowLoop(false);
            setPlaying(true);
        };

        const onPause = () => {
            setPlaying(false);
        };

        const onEnded = () => {
            setShowLoop(!active);
            setCounter(0);
            track({
                category: "adops",
                adOpsAction: "video",
                adOpsLabel: `video-completed`,
                event: "coreDataPush",
            });
        };

        const onUpdate = () => {
            setCurrent(Math.round(api?.plyr?.currentTime));

            if (api?.plyr?.paused === false) {
                if (current > 0.5) {
                    setProgress(
                        Number(
                            (api?.plyr?.currentTime / api?.plyr?.duration) * 100
                        )
                    );

                    if (progress > 1 && counter === 0) {
                        setCounter(1);
                        track({
                            category: "adops",
                            adOpsAction: "video",
                            adOpsLabel: `video-start`,
                            event: "coreDataPush",
                        });
                    }
                    if (progress > 25 && counter === 1) {
                        setCounter(2);
                        track({
                            category: "adops",
                            adOpsAction: "video",
                            adOpsLabel: `'video-played-25-percent`,
                            event: "coreDataPush",
                        });
                    }
                    if (progress > 50 && counter === 2) {
                        setCounter(3);
                        track({
                            category: "adops",
                            adOpsAction: "video",
                            adOpsLabel: `'video-played-50-percent`,
                            event: "coreDataPush",
                        });
                    }
                    if (progress > 75 && counter === 3) {
                        setCounter(4);
                        track({
                            category: "adops",
                            adOpsAction: "video",
                            adOpsLabel: `'video-played-75-percent`,
                            event: "coreDataPush",
                        });
                    }
                    if (progress < 5 && counter === 4) {
                        track({
                            category: "adops",
                            adOpsAction: "video",
                            adOpsLabel: `video-completed`,
                            event: "coreDataPush",
                        });
                        setShowLoop(!active);
                        track({
                            category: "adops",
                            adOpsAction: "video",
                            adOpsLabel: `video-start`,
                            event: "coreDataPush",
                        });
                        setCounter(1);
                    }
                }
            }
        };

        api?.plyr?.on("canplay", canPlay);
        api?.plyr?.on("playing", onPlay);
        api?.plyr?.on("pause", onPause);
        api?.plyr?.on("timeupdate", onUpdate);
        api?.plyr?.on("ended", onEnded);

        const offFuncs = () => {
            api?.plyr?.off("canplay", canPlay);
            api?.plyr?.off("playing", onPlay);
            api?.plyr?.off("pause", onPause);
            api?.plyr?.off("timeupdate", onUpdate);
            api?.plyr?.off("ended", onEnded);
        };

        if (ref?.current?.plyr?.source) {
            return offFuncs;
        }
    });

    if (!source || !ref) {
        return null;
    }

    return (
        <div
            data-size={size}
            data-full={full}
            className={styles.playercontainer}
        >
            {showLoopScreen || (!autoplay && current < 1 && !playing) ? (
                <div
                    className={styles.loopscreen}
                    style={{
                        backgroundImage: `url(${poster})`,
                    }}
                >
                    {!autoplay && current < 1 && !playing ? (
                        <svg
                            viewBox="0 0 503 612"
                            xmlns="http://www.w3.org/2000/svg"
                        >
                            <path
                                fill={ui?.color}
                                d="M0 608.7l503.2-302.6L0 3.5z"
                            />
                        </svg>
                    ) : (
                        <svg viewBox="0 0 425.47 402.84">
                            <g>
                                <path
                                    className="st0"
                                    fill="white"
                                    d="M138.18 396.9c-10.19-7.86-20.46-15.61-30.56-23.6-16.19-12.81-32.27-25.76-48.38-38.66-7.39-5.92-7.53-12.7-.3-18.51 24.41-19.58 48.87-39.09 73.26-58.69 4.09-3.28 8.27-5.07 13.27-2.24 3.92 2.22 5.33 5.76 5.31 10.13-.06 12.89-.02 25.78-.02 39.35h4.97c46.14 0 92.29.11 138.43-.04 41.76-.13 74.9-27.79 82.55-68.79 2.98-15.96.89-31.48-5.42-46.39-2.3-5.42-3.1-10.8-1.14-16.44 2.78-8.01 10.02-13.41 18.62-13.9 8.19-.47 16.06 4.3 19.68 12.32 7.4 16.4 10.93 33.63 10.72 51.64-.77 67.34-53.92 121.52-121.27 122.55-46.94.71-93.9.18-140.85.2h-6.3v22.05c0 4.84-.33 9.71.08 14.51.56 6.71-1.71 11.56-7.82 14.51h-4.83zM274.71 98.24c-2.04-.09-3.61-.23-5.18-.23-45.98-.01-91.96-.09-137.95.02-42 .1-75.29 27.77-82.89 68.98-2.94 15.95-.82 31.48 5.54 46.38 2.31 5.42 3 10.82.98 16.44-2.88 8.02-10.17 13.36-18.73 13.73-8.26.36-15.93-4.44-19.56-12.51-7.38-16.42-10.87-33.65-10.64-51.66.87-67.32 54.04-121.34 121.46-122.34 46.94-.7 93.9-.17 140.85-.19h6.12V25.92c0-2.42.11-4.85-.02-7.26-.27-5.08 1.12-9.31 5.98-11.67 4.95-2.4 8.95-.32 12.83 2.8 13.05 10.53 26.19 20.97 39.29 31.44 10.83 8.66 21.67 17.31 32.5 25.98 8.82 7.06 8.8 13.35-.03 20.4-23.56 18.83-47.11 37.68-70.66 56.52-1.01.81-2.02 1.6-3.05 2.38-3.54 2.68-7.34 3.23-11.31 1.1-3.75-2.01-5.52-5.26-5.52-9.51 0-11.45 0-22.91-.01-34.36v-5.5z"
                                />
                            </g>
                        </svg>
                    )}
                </div>
            ) : (
                <div
                    style={{
                        backgroundColor:
                            hexToRgb(gradient?.color, 0.75) ||
                            "rgba(0,0,0, .75)",
                    }}
                    className={styles.overlay}
                >
                    <div
                        style={{ backgroundColor: size === "one" && ui?.color }}
                        className={styles.circle}
                    >
                        {current < 60 && (
                            <span
                                style={{
                                    color:
                                        size === "one"
                                            ? gradient?.color
                                            : ui?.color,
                                }}
                            >
                                {current}
                            </span>
                        )}
                        <svg fill="rgba(0,0,0,0)" className={styles.progress}>
                            <circle
                                cx="25"
                                cy="25"
                                r="22"
                                style={{
                                    backgroundColor: "none",
                                    stroke:
                                        hexToRgb(`${ui?.color}`, 0.4) ||
                                        "white",
                                }}
                                className={styles.track}
                            />
                            <circle
                                cx="25"
                                cy="25"
                                r="22"
                                className={styles.pct}
                                style={{
                                    backgroundColor: "none",
                                    stroke: ui?.color,
                                    strokeDashoffset: `${
                                        (1 - progress / 200) *
                                        (2 * (22 / 7) * 40)
                                    }`,
                                }}
                            />
                        </svg>
                    </div>
                    <button
                        onClick={() => {
                            ref.current.plyr.togglePlay();
                        }}
                        className={styles.action}
                    >
                        {playing ? (
                            <svg
                                viewBox="0 0 503 594"
                                xmlns="http://www.w3.org/2000/svg"
                            >
                                <path
                                    fill={ui?.color}
                                    d="M2.2 2.2h174.2V594H2.2zM328.8 2.2H503V594H328.8z"
                                />
                            </svg>
                        ) : (
                            <svg
                                viewBox="0 0 503 612"
                                xmlns="http://www.w3.org/2000/svg"
                            >
                                <path
                                    fill={ui?.color}
                                    d="M0 608.7l503.2-302.6L0 3.5z"
                                />
                            </svg>
                        )}
                    </button>
                    <button
                        onClick={() => {
                            if (muted) {
                                ref.current.plyr.increaseVolume(1);
                            } else {
                                ref.current.plyr.decreaseVolume(1);
                            }
                        }}
                        className={styles.volume}
                    >
                        {muted ? (
                            <svg viewBox="0 0 510.75 511.76">
                                <path
                                    fill={ui?.color}
                                    d="M474.37 511.76h-1c-1.15-1.37-2.2-2.85-3.46-4.11-15.75-15.76-31.56-31.47-47.27-47.27-2.1-2.11-3.75-4.65-5.65-7.03-31.84 25.6-66.21 42.9-104.69 51.7-.32-.67-.59-.96-.59-1.25-.05-18.13-.14-36.25.05-54.37.01-1.32 1.92-3.19 3.38-3.83 9.26-4.07 18.98-7.21 27.89-11.92 11.17-5.91 21.7-13.02 32.04-19.33L254.62 293.9v188.32c-.42.19-.83.37-1.25.56-1.15-1.43-2.18-2.97-3.47-4.26-44.54-44.59-89.13-89.13-133.63-133.76-2.41-2.42-4.79-3.48-8.25-3.47-33.26.13-66.52.08-99.78.06-2.99 0-5.97-.16-8.96-.25V171.43c1.99-.09 3.97-.25 5.96-.25 40.56-.01 81.13-.01 121.69-.01h6.19c-1.83-2.01-2.78-3.12-3.81-4.15C87.13 124.8 44.93 82.6 2.71 40.43 1.68 39.41.44 38.6-.69 37.69v-1C11.03 24.46 22.75 12.24 34.3.19c159.26 159.26 317.32 317.32 476.51 476.5-12.04 11.58-24.24 23.32-36.44 35.07zM311.62 7.81c162.83 35.77 247.51 220.81 169.16 366.17-13.68-13.73-27.33-27.31-40.74-41.12-1.02-1.05-.8-3.88-.34-5.68 3.86-15.42 8.84-30.62 11.7-46.21 4.68-25.57 2-51.06-4.76-76.02-12.4-45.82-38.19-82.54-76.6-110.33-16.25-11.76-33.98-20.82-53.16-26.78-4.35-1.35-5.45-3.36-5.37-7.69.28-15.63.11-31.26.11-46.89V7.81z"
                                />
                                <path
                                    fill={ui?.color}
                                    d="M380.93 274.35c-6.71-6.8-12.73-12.96-18.82-19.06-15.84-15.88-31.74-31.7-47.51-47.65-1.38-1.39-2.83-3.53-2.85-5.33-.2-19.76-.13-39.53-.13-59.39 41.15 17.18 78.7 71.35 69.31 131.43zM196.95 88c18.71-18.67 38.08-37.99 57.47-57.33v114.81c-18.81-18.81-38.11-38.12-57.47-57.48z"
                                />
                            </svg>
                        ) : (
                            <svg viewBox="0 0 510.75 511.76">
                                <path
                                    fill={ui?.color}
                                    d="M0 170.93c2.98-.09 5.97-.25 8.95-.25 33.25-.02 66.51-.06 99.76.06 3.45.01 5.84-1.04 8.25-3.46 44.49-44.62 89.07-89.16 133.61-133.73 1.29-1.29 2.34-2.82 4.47-3.71v452.25c-1.55-1.43-2.87-2.57-4.1-3.8-44.57-44.55-89.15-89.08-133.63-133.71-2.67-2.67-5.3-3.83-9.1-3.82-34.08.14-68.17.09-102.25.07-1.99 0-3.98-.16-5.97-.25C0 284.04 0 227.48 0 170.93zM312.32 504.13v-35.44c0-5.99.24-11.99-.09-17.96-.2-3.65 1.11-5.22 4.53-6.29 21.61-6.77 41.38-17.18 59.28-31.04 39-30.19 64.03-69.36 73.94-117.68 17.18-83.77-20.14-167.68-94.04-211.03-12.36-7.25-26.17-12.05-39.45-17.67-3.04-1.29-4.31-2.52-4.27-6 .21-17.78.1-35.56.1-53.4 90.78 17.36 187.77 102.27 197.57 226.6 11.55 146.59-94.73 249.1-197.57 269.91z"
                                />
                                <path
                                    fill={ui?.color}
                                    d="M312.63 142.19c36.6 16.84 71.16 59.71 70.6 114.87-.57 55.95-36.91 97.18-70.6 112.3V142.19z"
                                />
                            </svg>
                        )}
                    </button>
                </div>
            )}
            <video
                ref={raptorRef as React.MutableRefObject<HTMLVideoElement>}
                className="plyr-react plyr"
            />
        </div>
    );
});

CustomPlyr.displayName = "CustomPlyr";

export default CustomPlyr;
realamirhe commented 2 years ago

What browser are you using on your iOS device? If you are using safari it seems that:

Safari browser doesn't support Ogg audio file type make sure you are not using this file type in your browser and make sure the extension of the audio file is correct.

Could you provide more info and post your founded solution to this issue too?

c-mella commented 2 years ago

@realamirhe This is happening on both Safari and Chrome on iOS and I'm using regular mp4 files. As of now I haven't found a solution for how to make this work yet.

chintan9 commented 2 years ago

iOS use webkit engine (safari under the hood).

Can check event is triggering or not?

Thanks for reporting.

c-mella commented 2 years ago

Ok so I ran a test where i set the function to either increase or decrease based on the volume coming from the player and no matter what happened it always defaulted to decreaseVolume. This leads me to believe that the problem is in the connection between the player ref and the controls. For reference the togglePlay function will always fire and work properly. Same as before this worked fine on desktop but on mobile it's not working.

here's the function code i used for the test

onClick={() => {
  if (ref?.current?.plyr?.volume < 1) {
      console.log("event fired to increase");
      setMuted(false);
      ref.current.plyr.increaseVolume(1);
  } else {
      console.log("event fired to decrease");
      setMuted(true);
      ref.current.plyr.decreaseVolume(1);
  }
}}
c-mella commented 2 years ago

I also ran a test where I used the custom metric of "muted" to set the function but in that case even though the event registered and printed "event fired to decrease", the audio would be stuck at 1.

Here's the code for context:

onClick={() => {
  if (muted) {
      console.log("event fired to increase");
      setMuted(false);
      ref.current.plyr.increaseVolume(1);
  } else {
      console.log("event fired to decrease");
      setMuted(true);
      ref.current.plyr.decreaseVolume(1);
  }
}}
realamirhe commented 2 years ago

Thanks for the detailed test @c-mella; could you put log (alert) down on the plyr increaseVolume? Or even simpler could you make the same functionality using plyr in codepen and test it on safari? Here is a simple starter kit (codepen)

realamirhe commented 2 years ago

I also ran a test where I used the custom metric of "muted" to set the function but in that case even though the event registered and printed "event fired to decrease", the audio would be stuck at 1.

Here's the code for context:

onClick={() => {
  if (muted) {
      console.log("event fired to increase");
      setMuted(false);
      ref.current.plyr.increaseVolume(1);
  } else {
      console.log("event fired to decrease");
      setMuted(true);
      ref.current.plyr.decreaseVolume(1);
  }
}}

How and when do you grab the muted property?

c-mella commented 2 years ago

How and when do you grab the muted property?

I'll work on getting you that detailed CodePen test, in the meantime here's a breakdown of the muted property.

In the original code snippet I set it in using useState with a default value of true and then update it using useEffect whenever there is a change in volume (This work on Desktop but not mobile)

In the subsequent examples I was setting again setting it using useState but then updating it manually whenever the button is clicked. In this case the button would update the value and fire the console log but the decreaseVolume action wouldn't fire for some reason. Meaning that the button would change but the volume would remain at 1

realamirhe commented 2 years ago

Hmm, that's weird, in case you don't get an error in calling the function it means that you get some proper instance of Plyr instead of the default proxy.

I'm waiting for your codepen test, so we can trace down the issue more properly. Thanks for your effort @c-mella.

c-mella commented 2 years ago

I removed a lot of the dynamic aspects of the code and replaced it with hard coded values in order to make the test work but here is the sample code for you to review.

https://codepen.io/c-mella/pen/JjvPRjM

Thanks again for you help

c-mella commented 2 years ago

Hi, just wanted to see if you'd had a chance to review? I just went in and revised the pen with a new video because I realized the previous one had very subtle audio so it wouldnt be easy to test.

realamirhe commented 2 years ago

oh sorry for delay @c-mella,

I might miss the notification or didn't get one, please directly mention me if I didn't respond.

I'm going to review it today. Thanks for your help ❤️

realamirhe commented 2 years ago

Hey @c-mella despite extra unnecessary re-render the code seems to find at the code level, and functional in all available platforms for me.

I changed it a little bit by adding the observed variable at the top of the video for testing on mobile (iOS)

codepen

c-mella commented 2 years ago

@realamirhe Thanks again for looking into it, I'm not sure what the issue is but I and my teammates are still seeing the video as muted no matter how much we click the unmute button. I'm not sure if this helps but I'm on an iPhone 13 running iOS 15.6