Compare commits

...

4 Commits

Author SHA1 Message Date
Scott Lamb 9acb095a5d prep v0.7.14 2024-04-16 21:07:34 -07:00
Scott Lamb 8b5f2b4b0d 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.)
2024-04-16 17:08:16 -07:00
Scott Lamb a65994ba71 match VS Code extension rename 2024-04-16 16:44:36 -07:00
Scott Lamb ef98f60241 mention michioxd's #315 changes 2024-04-16 16:43:57 -07:00
5 changed files with 62 additions and 45 deletions

View File

@ -8,7 +8,7 @@
"esbenp.prettier-vscode",
"rust-lang.rust-analyzer",
"yzhang.markdown-all-in-one",
"zixuanchen.vitest-explorer"
"vitest.explorer"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": [

View File

@ -1,3 +1,4 @@
Scott Lamb <slamb@slamb.org>
Dolf Starreveld <dolf@starreveld.com>
Sky1e <me@skye-c.at>
Sky1e <me@skye-c.at>
michioxd <michio.haiyaku@gmail.com>

View File

@ -8,6 +8,21 @@ upgrades, e.g. `v0.6.x` -> `v0.7.x`. The config file format and
[API](ref/api.md) currently have no stability guarantees, so they may change
even on minor releases, e.g. `v0.7.5` -> `v0.7.6`.
## v0.7.14 (2024-04-16)
* Many UI improvements in [#315](https://github.com/scottlamb/moonfire-nvr/pull/315)
from [@michioxd](https://github.com/michioxd). See the PR description for
full details, including screenshots.
* dark/light modes
* redesigned login dialog
* live view: new dual camera layout, more descriptive layout names,
full screen option, re-open with last layout and camera selection
* 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)
* seamlessly merge together recordings which have imperceptible changes in

View File

@ -26,10 +26,10 @@ left, and pick the [latest tagged version](https://github.com/scottlamb/moonfire
Download the binary for your platform from the matching GitHub release.
Install it as `/usr/local/bin/moonfire-nvr` and ensure it is executable, e.g.
for version `v0.7.11`:
for version `v0.7.14`:
```console
$ VERSION=v0.7.11
$ VERSION=v0.7.14
$ ARCH=$(uname -m)
$ curl -OL "https://github.com/scottlamb/moonfire-nvr/releases/download/$VERSION/moonfire-nvr-$VERSION-$ARCH"
$ sudo install -m 755 "moonfire-nvr-$VERSION-$ARCH" /usr/local/bin/moonfire-nvr

View File

@ -2,7 +2,7 @@
// 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
import React, { SyntheticEvent } from "react";
import React from "react";
import { Camera } from "../types";
import { Part, parsePart } from "./parser";
import * as api from "../api";
@ -63,15 +63,34 @@ class LiveCameraDriver {
camera: Camera,
setPlaybackState: (state: PlaybackState) => void,
setAspect: (aspect: [number, number]) => void,
videoRef: React.RefObject<HTMLVideoElement>
video: HTMLVideoElement
) {
this.camera = camera;
this.setPlaybackState = setPlaybackState;
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.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 = () => {
this.startStream("sourceopen");
};
@ -252,12 +271,11 @@ class LiveCameraDriver {
if (
this.buf.state !== "open" ||
this.buf.busy ||
this.buf.srcBuf.buffered.length === 0 ||
this.videoRef.current === null
this.buf.srcBuf.buffered.length === 0
) {
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
// to avoid removing anything from the current GOP.
@ -273,13 +291,23 @@ class LiveCameraDriver {
this.error(`SourceBuffer ${e.type}`);
};
videoPlaying = (e: SyntheticEvent<HTMLVideoElement, Event>) => {
videoPause = () => {
this.stopStream("pause");
};
videoPlay = () => {
this.startStream("play");
};
videoPlaying = () => {
if (this.buf.state !== "error") {
this.setPlaybackState({ state: "normal" });
}
};
videoWaiting = (e: SyntheticEvent<HTMLVideoElement, Event>) => {
videoTimeUpdate = () => {};
videoWaiting = () => {
if (this.buf.state !== "error") {
this.setPlaybackState({ state: "waiting" });
}
@ -302,7 +330,7 @@ class LiveCameraDriver {
camera: Camera;
setPlaybackState: (state: PlaybackState) => void;
setAspect: (aspect: [number, number]) => void;
videoRef: React.RefObject<HTMLVideoElement>;
video: HTMLVideoElement;
src = new MediaSource();
buf: BufferState = { state: "closed" };
@ -321,7 +349,6 @@ class LiveCameraDriver {
* 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
* and unmounting a camera.
*
*/
const LiveCamera = ({ camera, chooser }: LiveCameraProps) => {
const [aspect, setAspect] = React.useState<[number, number]>([16, 9]);
@ -339,25 +366,15 @@ const LiveCamera = ({ camera, chooser }: LiveCameraProps) => {
});
// Load the camera driver.
const [driver, setDriver] = React.useState<LiveCameraDriver | null>(null);
React.useEffect(() => {
setPlaybackState({ state: "normal" });
if (camera === null) {
setDriver(null);
const video = videoRef.current;
if (camera === null || video === null) {
return;
}
const d = new LiveCameraDriver(
camera,
setPlaybackState,
setAspect,
videoRef
);
setDriver(d);
const d = new LiveCameraDriver(camera, setPlaybackState, setAspect, video);
return () => {
// Explictly stop the stream on unmount. There don't seem to be any DOM
// event handlers that run in this case. (In particular, the MediaSource's
// sourceclose doesn't run.)
d.stopStream("unmount or camera change");
d.unmount();
};
}, [camera]);
@ -372,22 +389,6 @@ const LiveCamera = ({ camera, chooser }: LiveCameraProps) => {
return () => clearTimeout(timerId);
}, [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 (
<Box
ref={boxRef}
@ -446,7 +447,7 @@ const LiveCamera = ({ camera, chooser }: LiveCameraProps) => {
<Alert severity="error">{playbackState.message}</Alert>
</div>
)}
{videoElement}
<video ref={videoRef} muted autoPlay />
</Box>
);
};