liqvidjs / liqvid

Library for interactive videos in React
https://liqvidjs.org
MIT License
761 stars 39 forks source link

Getting an error when refreshing the player? #23

Closed anishg-cn closed 2 years ago

anishg-cn commented 2 years ago

I am getting the following error when I refresh the player after a change in the script:

Uncaught TypeError: Cannot read properties of null (reading 'buffered')
    at getBuffers (liqvid.js:895:1)
    at HTMLAudioElement.updateBuffers (liqvid.js:907:1)

What i am doing is, I have a state variable initialized, and i have used useEffect to check when there is a change in the script. In the useEffect i have set the initialized value to false and then to true with a setTimeout so that the player is removed from dom and re-rendered again so that the script change is reflected.

const [initialized, setInitialized] = useState(false);

useEffect(() => {
  setInitialized(false);
  setTimeout(() => {
    setInitialized(true);
  }, 0);
}, [script]);

if (!initialized) {
  return null;
}

return <Player ... ></Player>

Can you please tell if I am doing anything wrong, or is there a way to resolve the issue?

anishg-cn commented 2 years ago

@ysulyma please check this, its very urgent.

ysulyma commented 2 years ago

The snippet you posted worked for me, can you share your full code somehow?

Does this happen when the video is playing and you swap out the DOM, or even when it's paused?

anishg-cn commented 2 years ago

@ysulyma yes when the video is playing, and sorry i cannot share the full code.

ysulyma commented 2 years ago

I'll keep investigating this, but it's probably safer to switch out the script inside the video. The process for doing this isn't very smooth, but would look like:

import {Script, usePlayer} from "liqvid";
const script2 = new Script([
  ["example", "2:00"]
]);

const player = usePlayer();
const change = useCallback(() => {
  // update markers and duration
  player.script.markers = script2.markers;
  player.script.playback.duration = script2.playback.duration;
}, []);
anishg-cn commented 2 years ago

@ysulyma when the script changes, will the <Player script={script} /> not get updated automatically?

ysulyma commented 2 years ago

@anishg-cn is your issue that the hiding feature of from() and during will not get updated? Or the contents of the video?

anishg-cn commented 2 years ago

@ysulyma it looks like the script is not updating in the player, when the script is changed in the parent component. So what happens is after updating the script if i try to call script.playback.pause() from the parent component, the player is not pausing, as if the script is not updated.

ysulyma commented 2 years ago

@anishg-cn yeah, so it's best to copy over the markers and playback duration to the existing script rather than replacing it. Playback emits a durationchange event when its duration gets changed, so the time display should automatically update.

I think I know what's causing the getBuffers error. In the meantime, let me know if this does what you want:

import * as ReactDOM from "react-dom";

import {Audio, Player, Script, Utils, usePlayer} from "liqvid";
import {useEffect, useMemo, useState} from "react";

const {during, from} = Utils.authoring;
const {onClick} = Utils.mobile;

/* the leading | character is because during("") doesn't currently work */
const script1 = new Script([
  ["|animal", "1:00"],
  ["|animal/cow", "1:00"]
]);

const script2 = new Script([
  ["|fruit", "1:00"],
  ["|fruit/like", "1:00"],
]);

function Lesson() {
  return (
    <Player script={script1}>
      <Switcher />
    </Player>
  );
}

function Switcher() {
  const player = usePlayer();
  const [mode, setMode] = useState("animal");

  // swap scripts
  const events = useMemo(() => onClick<HTMLButtonElement>((e) => {
    // blur button so keyboard shortcuts keep working
    e.currentTarget.blur();

    // update script
    player.script.markers = script2.markers;
    player.script.playback.duration = script2.playback.duration;

    // update mode
    setMode("fruit");
  }), []);

  return (
    <div id="root" {...during("|")}> {/* wrapper element is needed due to a bug in player.reparseTree */}
      <button {...events}>Change script</button>
      {mode === "animal" ? <Content1 /> : <Content2 />}
    </div>
  );
}

function Content1() {
  return (
    <section>
      <Audio start={0}>
        <source src="./audio/animals.mp4" type="audio/mp4" />
        <source src="./audio/animals.webm" type="audio/webm" />
      </Audio>
      This video is about animals
      <img alt="Cow" src="./cow.jpg" {...from("|animal/cow")} />
    </section>
  );
}

function Content2() {
  const player = usePlayer();

  useEffect(() => {
    // re-apply during() and from()
    // in future you'll be able to just do player.reparseTree(player.canvas) here,
    // but currently there's a bug
    /* @ts-ignore reparseTree isn't exposed in Liqvid 2.0, will be in 2.1 */
    player.reparseTree(player.canvas.querySelector("[data-during]"));
  }, []);

  return (
    <section>
      <Audio start={0}>
        <source src="./audio/fruit.mp4" type="audio/mp4" />
        <source src="./audio/fruit.webm" type="audio/webm" />
      </Audio>
      This video is about fruit:
      I like <span {...from("|fruit/like")}>apples</span>
    </section>
  );
}

ReactDOM.render(<Lesson />, document.querySelector("main"));
anishg-cn commented 2 years ago

@ysulyma do I need to use the following code when script changes? if yes, can you please explain what it is doing?

useEffect(() => {
  // re-apply during() and from()
  // in future you'll be able to just do player.reparseTree(player.canvas) here,
  // but currently there's a bug
  /* @ts-ignore reparseTree isn't exposed in Liqvid 2.0, will be in 2.1 */
  player.reparseTree(player.canvas.querySelector("[data-during]"));
}, []);

I am currently using liqvid version ^2.0.10

anishg-cn commented 2 years ago

@ysulyma Also if i change the audio src, it is not reflecting in the player. Can you tell me if there is a way to change the audio src like the script?

<Audio ref={audioRef} start={0} obstructCanPlayThrough>
  <source src={audioSrc} />
</Audio>
ysulyma commented 2 years ago

@anishg-cn you need to use that if you're using during() and from(). Internally, those work by attaching data-during, data-from-first, and data-from-last attributes. When the player is first loaded, it scans its contents for elements with these attributes, and sets up a listener to update the tree when the active marker changes. If new elements are added which have these attributes, the player needs to re-parse that part of the tree.

Even if you're not changing the script, you need to use that if you're adding new elements to the DOM which use data-during/data-from-first/data-from-last, e.g. if you're using those inside MathJax or KaTeX.

ysulyma commented 2 years ago

@anishg-cn as for the audio src, you can either replace the <Audio> element entirely like I did in the example above (but it sounds like that was giving you errors), or you can change the src attribute on the <Audio> element instead of using <source> (not ideal for iOS).

The HTML5 spec says:

Dynamically modifying a source element and its attribute when the element is already inserted in a video or audio element will have no effect. To change what is playing, just use the src attribute on the media element directly, possibly making use of the canPlayType() method to pick from amongst available resources. Generally, manipulating source elements manually after the document has been parsed is an unncessarily[sic] complicated approach.

MDN says:

Note: If the src property is updated (along with any siblings), the parent HTMLMediaElement's load method should be called when done, since <source> elements are not re-scanned automatically.

But that doesn't work as well with the declarative approach. Maybe you could do this with audioRef.current.domElement.load(). I think I'd suggest using JS to decide whether to serve an mp4 or webm, and then use src on <Audio> directly rather than <source> elements.

Btw, this sounds really cool! I experimented just a little bit with dynamic scripts/audio, it's exciting to see someone doing this.