mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-01-12 15:33:22 -05:00
work on Firefox!
Fixes #286. I dug into Firefox code a bit to understand this but got a lost. But I guess there are different policies for what's accessible via `video.src = ""` than for `<video src="">`. This works for now, matches what other players such as `mpegts.js` do, and is closer to what Safari MME (required on iPhone) needs anyway. (With MME, apparently you have to use `video.srcObject`, or the `MediaSource`'s `sourceopen` event will never fire.)
This commit is contained in:
parent
a65994ba71
commit
8b5f2b4b0d
@ -18,6 +18,10 @@ even on minor releases, e.g. `v0.7.5` -> `v0.7.6`.
|
|||||||
* live view: new dual camera layout, more descriptive layout names,
|
* live view: new dual camera layout, more descriptive layout names,
|
||||||
full screen option, re-open with last layout and camera selection
|
full screen option, re-open with last layout and camera selection
|
||||||
* list view: filter button becomes outlined when enabled
|
* list view: filter button becomes outlined when enabled
|
||||||
|
* Fix [#286](https://github.com/scottlamb/moonfire-nvr/issues/286):
|
||||||
|
live view now works on Firefox! Formerly, it'd fail with messages such as
|
||||||
|
`Security Error: Content at https://mydomain.com/ may not load data from blob:https://mydomain.com/44abc5dc-750d-48d1-817d-2e6a52445592`.
|
||||||
|
|
||||||
|
|
||||||
## v0.7.13 (2024-02-12)
|
## v0.7.13 (2024-02-12)
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// Copyright (C) 2021 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
// Copyright (C) 2021 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
|
||||||
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
|
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception
|
||||||
|
|
||||||
import React, { SyntheticEvent } from "react";
|
import React from "react";
|
||||||
import { Camera } from "../types";
|
import { Camera } from "../types";
|
||||||
import { Part, parsePart } from "./parser";
|
import { Part, parsePart } from "./parser";
|
||||||
import * as api from "../api";
|
import * as api from "../api";
|
||||||
@ -63,15 +63,34 @@ class LiveCameraDriver {
|
|||||||
camera: Camera,
|
camera: Camera,
|
||||||
setPlaybackState: (state: PlaybackState) => void,
|
setPlaybackState: (state: PlaybackState) => void,
|
||||||
setAspect: (aspect: [number, number]) => void,
|
setAspect: (aspect: [number, number]) => void,
|
||||||
videoRef: React.RefObject<HTMLVideoElement>
|
video: HTMLVideoElement
|
||||||
) {
|
) {
|
||||||
this.camera = camera;
|
this.camera = camera;
|
||||||
this.setPlaybackState = setPlaybackState;
|
this.setPlaybackState = setPlaybackState;
|
||||||
this.setAspect = setAspect;
|
this.setAspect = setAspect;
|
||||||
this.videoRef = videoRef;
|
this.video = video;
|
||||||
|
video.addEventListener("pause", this.videoPause);
|
||||||
|
video.addEventListener("play", this.videoPlay);
|
||||||
|
video.addEventListener("playing", this.videoPlaying);
|
||||||
|
video.addEventListener("timeupdate", this.videoTimeUpdate);
|
||||||
|
video.addEventListener("waiting", this.videoWaiting);
|
||||||
this.src.addEventListener("sourceopen", this.onMediaSourceOpen);
|
this.src.addEventListener("sourceopen", this.onMediaSourceOpen);
|
||||||
|
this.video.src = this.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unmount = () => {
|
||||||
|
this.stopStream("unmount");
|
||||||
|
const v = this.video;
|
||||||
|
v.removeEventListener("pause", this.videoPause);
|
||||||
|
v.removeEventListener("play", this.videoPlay);
|
||||||
|
v.removeEventListener("playing", this.videoPlaying);
|
||||||
|
v.removeEventListener("timeupdate", this.videoTimeUpdate);
|
||||||
|
v.removeEventListener("waiting", this.videoWaiting);
|
||||||
|
v.src = "";
|
||||||
|
v.load();
|
||||||
|
URL.revokeObjectURL(this.url);
|
||||||
|
};
|
||||||
|
|
||||||
onMediaSourceOpen = () => {
|
onMediaSourceOpen = () => {
|
||||||
this.startStream("sourceopen");
|
this.startStream("sourceopen");
|
||||||
};
|
};
|
||||||
@ -252,12 +271,11 @@ class LiveCameraDriver {
|
|||||||
if (
|
if (
|
||||||
this.buf.state !== "open" ||
|
this.buf.state !== "open" ||
|
||||||
this.buf.busy ||
|
this.buf.busy ||
|
||||||
this.buf.srcBuf.buffered.length === 0 ||
|
this.buf.srcBuf.buffered.length === 0
|
||||||
this.videoRef.current === null
|
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const curTs = this.videoRef.current.currentTime;
|
const curTs = this.video.currentTime;
|
||||||
|
|
||||||
// TODO: call out key frames in the part headers. The "- 5" here is a guess
|
// TODO: call out key frames in the part headers. The "- 5" here is a guess
|
||||||
// to avoid removing anything from the current GOP.
|
// to avoid removing anything from the current GOP.
|
||||||
@ -273,13 +291,23 @@ class LiveCameraDriver {
|
|||||||
this.error(`SourceBuffer ${e.type}`);
|
this.error(`SourceBuffer ${e.type}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
videoPlaying = (e: SyntheticEvent<HTMLVideoElement, Event>) => {
|
videoPause = () => {
|
||||||
|
this.stopStream("pause");
|
||||||
|
};
|
||||||
|
|
||||||
|
videoPlay = () => {
|
||||||
|
this.startStream("play");
|
||||||
|
};
|
||||||
|
|
||||||
|
videoPlaying = () => {
|
||||||
if (this.buf.state !== "error") {
|
if (this.buf.state !== "error") {
|
||||||
this.setPlaybackState({ state: "normal" });
|
this.setPlaybackState({ state: "normal" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
videoWaiting = (e: SyntheticEvent<HTMLVideoElement, Event>) => {
|
videoTimeUpdate = () => {};
|
||||||
|
|
||||||
|
videoWaiting = () => {
|
||||||
if (this.buf.state !== "error") {
|
if (this.buf.state !== "error") {
|
||||||
this.setPlaybackState({ state: "waiting" });
|
this.setPlaybackState({ state: "waiting" });
|
||||||
}
|
}
|
||||||
@ -302,7 +330,7 @@ class LiveCameraDriver {
|
|||||||
camera: Camera;
|
camera: Camera;
|
||||||
setPlaybackState: (state: PlaybackState) => void;
|
setPlaybackState: (state: PlaybackState) => void;
|
||||||
setAspect: (aspect: [number, number]) => void;
|
setAspect: (aspect: [number, number]) => void;
|
||||||
videoRef: React.RefObject<HTMLVideoElement>;
|
video: HTMLVideoElement;
|
||||||
|
|
||||||
src = new MediaSource();
|
src = new MediaSource();
|
||||||
buf: BufferState = { state: "closed" };
|
buf: BufferState = { state: "closed" };
|
||||||
@ -321,7 +349,6 @@ class LiveCameraDriver {
|
|||||||
* Note there's a significant setup cost to creating a LiveCamera, so the parent
|
* Note there's a significant setup cost to creating a LiveCamera, so the parent
|
||||||
* should use React's <tt>key</tt> attribute to avoid unnecessarily mounting
|
* should use React's <tt>key</tt> attribute to avoid unnecessarily mounting
|
||||||
* and unmounting a camera.
|
* and unmounting a camera.
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
const LiveCamera = ({ camera, chooser }: LiveCameraProps) => {
|
const LiveCamera = ({ camera, chooser }: LiveCameraProps) => {
|
||||||
const [aspect, setAspect] = React.useState<[number, number]>([16, 9]);
|
const [aspect, setAspect] = React.useState<[number, number]>([16, 9]);
|
||||||
@ -339,25 +366,15 @@ const LiveCamera = ({ camera, chooser }: LiveCameraProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Load the camera driver.
|
// Load the camera driver.
|
||||||
const [driver, setDriver] = React.useState<LiveCameraDriver | null>(null);
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setPlaybackState({ state: "normal" });
|
setPlaybackState({ state: "normal" });
|
||||||
if (camera === null) {
|
const video = videoRef.current;
|
||||||
setDriver(null);
|
if (camera === null || video === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const d = new LiveCameraDriver(
|
const d = new LiveCameraDriver(camera, setPlaybackState, setAspect, video);
|
||||||
camera,
|
|
||||||
setPlaybackState,
|
|
||||||
setAspect,
|
|
||||||
videoRef
|
|
||||||
);
|
|
||||||
setDriver(d);
|
|
||||||
return () => {
|
return () => {
|
||||||
// Explictly stop the stream on unmount. There don't seem to be any DOM
|
d.unmount();
|
||||||
// event handlers that run in this case. (In particular, the MediaSource's
|
|
||||||
// sourceclose doesn't run.)
|
|
||||||
d.stopStream("unmount or camera change");
|
|
||||||
};
|
};
|
||||||
}, [camera]);
|
}, [camera]);
|
||||||
|
|
||||||
@ -372,22 +389,6 @@ const LiveCamera = ({ camera, chooser }: LiveCameraProps) => {
|
|||||||
return () => clearTimeout(timerId);
|
return () => clearTimeout(timerId);
|
||||||
}, [playbackState]);
|
}, [playbackState]);
|
||||||
|
|
||||||
const videoElement =
|
|
||||||
driver === null ? (
|
|
||||||
<video />
|
|
||||||
) : (
|
|
||||||
<video
|
|
||||||
ref={videoRef}
|
|
||||||
muted
|
|
||||||
autoPlay
|
|
||||||
src={driver.url}
|
|
||||||
onPause={() => driver.stopStream("pause")}
|
|
||||||
onPlay={() => driver.startStream("play")}
|
|
||||||
onPlaying={driver.videoPlaying}
|
|
||||||
onTimeUpdate={driver.tryTrimBuffer}
|
|
||||||
onWaiting={driver.videoWaiting}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
ref={boxRef}
|
ref={boxRef}
|
||||||
@ -446,7 +447,7 @@ const LiveCamera = ({ camera, chooser }: LiveCameraProps) => {
|
|||||||
<Alert severity="error">{playbackState.message}</Alert>
|
<Alert severity="error">{playbackState.message}</Alert>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{videoElement}
|
<video ref={videoRef} muted autoPlay />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user