mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-03-30 17:23:43 -04:00
attempt at iPhone support (#121)
This commit is contained in:
parent
9acb095a5d
commit
93a9ad9af3
@ -12,7 +12,20 @@ import Alert from "@mui/material/Alert";
|
|||||||
import useResizeObserver from "@react-hook/resize-observer";
|
import useResizeObserver from "@react-hook/resize-observer";
|
||||||
import { fillAspect } from "../aspect";
|
import { fillAspect } from "../aspect";
|
||||||
|
|
||||||
|
/// The media source API to use:
|
||||||
|
/// * Essentially everything but iPhone supports `MediaSource`.
|
||||||
|
/// (All major desktop browsers; Android browsers; and Safari on iPad are
|
||||||
|
/// fine.)
|
||||||
|
/// * Safari/macOS and Safari/iPhone on iOS 17+ support `ManagedMediaSource`.
|
||||||
|
/// * Safari/iPhone with older iOS does not support anything close to
|
||||||
|
/// `MediaSource`.
|
||||||
|
export const MediaSourceApi: typeof MediaSource | undefined =
|
||||||
|
(self as any).ManagedMediaSource ?? self.MediaSource;
|
||||||
|
|
||||||
interface LiveCameraProps {
|
interface LiveCameraProps {
|
||||||
|
/// Caller should provide a failure path when `MediaSourceApi` is undefined
|
||||||
|
/// and pass it back here otherwise.
|
||||||
|
mediaSourceApi: typeof MediaSource;
|
||||||
camera: Camera | null;
|
camera: Camera | null;
|
||||||
chooser: JSX.Element;
|
chooser: JSX.Element;
|
||||||
}
|
}
|
||||||
@ -60,11 +73,14 @@ type PlaybackState =
|
|||||||
*/
|
*/
|
||||||
class LiveCameraDriver {
|
class LiveCameraDriver {
|
||||||
constructor(
|
constructor(
|
||||||
|
mediaSourceApi: typeof MediaSource,
|
||||||
camera: Camera,
|
camera: Camera,
|
||||||
setPlaybackState: (state: PlaybackState) => void,
|
setPlaybackState: (state: PlaybackState) => void,
|
||||||
setAspect: (aspect: [number, number]) => void,
|
setAspect: (aspect: [number, number]) => void,
|
||||||
video: HTMLVideoElement
|
video: HTMLVideoElement
|
||||||
) {
|
) {
|
||||||
|
this.mediaSourceApi = mediaSourceApi;
|
||||||
|
this.src = new mediaSourceApi();
|
||||||
this.camera = camera;
|
this.camera = camera;
|
||||||
this.setPlaybackState = setPlaybackState;
|
this.setPlaybackState = setPlaybackState;
|
||||||
this.setAspect = setAspect;
|
this.setAspect = setAspect;
|
||||||
@ -75,7 +91,12 @@ class LiveCameraDriver {
|
|||||||
video.addEventListener("timeupdate", this.videoTimeUpdate);
|
video.addEventListener("timeupdate", this.videoTimeUpdate);
|
||||||
video.addEventListener("waiting", this.videoWaiting);
|
video.addEventListener("waiting", this.videoWaiting);
|
||||||
this.src.addEventListener("sourceopen", this.onMediaSourceOpen);
|
this.src.addEventListener("sourceopen", this.onMediaSourceOpen);
|
||||||
this.video.src = this.url;
|
|
||||||
|
// This appears necessary for the `ManagedMediaSource` API to function
|
||||||
|
// on Safari/iOS.
|
||||||
|
video["disableRemotePlayback"] = true;
|
||||||
|
video.src = this.objectUrl = URL.createObjectURL(this.src);
|
||||||
|
video.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
unmount = () => {
|
unmount = () => {
|
||||||
@ -87,8 +108,8 @@ class LiveCameraDriver {
|
|||||||
v.removeEventListener("timeupdate", this.videoTimeUpdate);
|
v.removeEventListener("timeupdate", this.videoTimeUpdate);
|
||||||
v.removeEventListener("waiting", this.videoWaiting);
|
v.removeEventListener("waiting", this.videoWaiting);
|
||||||
v.src = "";
|
v.src = "";
|
||||||
|
URL.revokeObjectURL(this.objectUrl);
|
||||||
v.load();
|
v.load();
|
||||||
URL.revokeObjectURL(this.url);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMediaSourceOpen = () => {
|
onMediaSourceOpen = () => {
|
||||||
@ -169,7 +190,7 @@ class LiveCameraDriver {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const part = result.part;
|
const part = result.part;
|
||||||
if (!MediaSource.isTypeSupported(part.mimeType)) {
|
if (!this.mediaSourceApi.isTypeSupported(part.mimeType)) {
|
||||||
this.error(`unsupported mime type ${part.mimeType}`);
|
this.error(`unsupported mime type ${part.mimeType}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -332,13 +353,14 @@ class LiveCameraDriver {
|
|||||||
setAspect: (aspect: [number, number]) => void;
|
setAspect: (aspect: [number, number]) => void;
|
||||||
video: HTMLVideoElement;
|
video: HTMLVideoElement;
|
||||||
|
|
||||||
src = new MediaSource();
|
mediaSourceApi: typeof MediaSource;
|
||||||
|
src: MediaSource;
|
||||||
buf: BufferState = { state: "closed" };
|
buf: BufferState = { state: "closed" };
|
||||||
queue: Part[] = [];
|
queue: Part[] = [];
|
||||||
queuedBytes: number = 0;
|
queuedBytes: number = 0;
|
||||||
|
|
||||||
/// The object URL for the HTML video element, not the WebSocket URL.
|
/// The object URL for the HTML video element, not the WebSocket URL.
|
||||||
url = URL.createObjectURL(this.src);
|
objectUrl: string;
|
||||||
|
|
||||||
ws?: WebSocket;
|
ws?: WebSocket;
|
||||||
}
|
}
|
||||||
@ -350,7 +372,7 @@ class LiveCameraDriver {
|
|||||||
* 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 = ({ mediaSourceApi, camera, chooser }: LiveCameraProps) => {
|
||||||
const [aspect, setAspect] = React.useState<[number, number]>([16, 9]);
|
const [aspect, setAspect] = React.useState<[number, number]>([16, 9]);
|
||||||
const videoRef = React.useRef<HTMLVideoElement>(null);
|
const videoRef = React.useRef<HTMLVideoElement>(null);
|
||||||
const boxRef = React.useRef<HTMLElement>(null);
|
const boxRef = React.useRef<HTMLElement>(null);
|
||||||
@ -372,11 +394,17 @@ const LiveCamera = ({ camera, chooser }: LiveCameraProps) => {
|
|||||||
if (camera === null || video === null) {
|
if (camera === null || video === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const d = new LiveCameraDriver(camera, setPlaybackState, setAspect, video);
|
const d = new LiveCameraDriver(
|
||||||
|
mediaSourceApi,
|
||||||
|
camera,
|
||||||
|
setPlaybackState,
|
||||||
|
setAspect,
|
||||||
|
video
|
||||||
|
);
|
||||||
return () => {
|
return () => {
|
||||||
d.unmount();
|
d.unmount();
|
||||||
};
|
};
|
||||||
}, [camera]);
|
}, [mediaSourceApi, camera]);
|
||||||
|
|
||||||
// Display circular progress after 100 ms of waiting.
|
// Display circular progress after 100 ms of waiting.
|
||||||
const [showProgress, setShowProgress] = React.useState(false);
|
const [showProgress, setShowProgress] = React.useState(false);
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
import Container from "@mui/material/Container";
|
import Container from "@mui/material/Container";
|
||||||
import ErrorIcon from "@mui/icons-material/Error";
|
import ErrorIcon from "@mui/icons-material/Error";
|
||||||
import { Camera } from "../types";
|
import { Camera } from "../types";
|
||||||
import LiveCamera from "./LiveCamera";
|
import LiveCamera, { MediaSourceApi } from "./LiveCamera";
|
||||||
import Multiview, { MultiviewChooser } from "./Multiview";
|
import Multiview, { MultiviewChooser } from "./Multiview";
|
||||||
import { FrameProps } from "../App";
|
import { FrameProps } from "../App";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
@ -36,7 +36,8 @@ const Live = ({ cameras, Frame }: LiveProps) => {
|
|||||||
);
|
);
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
|
|
||||||
if ("MediaSource" in window === false) {
|
const mediaSourceApi = MediaSourceApi;
|
||||||
|
if (mediaSourceApi === undefined) {
|
||||||
return (
|
return (
|
||||||
<Frame>
|
<Frame>
|
||||||
<Container>
|
<Container>
|
||||||
@ -72,7 +73,11 @@ const Live = ({ cameras, Frame }: LiveProps) => {
|
|||||||
layoutIndex={multiviewLayoutIndex}
|
layoutIndex={multiviewLayoutIndex}
|
||||||
cameras={cameras}
|
cameras={cameras}
|
||||||
renderCamera={(camera: Camera | null, chooser: JSX.Element) => (
|
renderCamera={(camera: Camera | null, chooser: JSX.Element) => (
|
||||||
<LiveCamera camera={camera} chooser={chooser} />
|
<LiveCamera
|
||||||
|
mediaSourceApi={mediaSourceApi}
|
||||||
|
camera={camera}
|
||||||
|
chooser={chooser}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Frame>
|
</Frame>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user