feat: Enhance support for a wider variety of video URL formats. #101

Open
nickydev wants to merge 4 commits from nickydev/stream:main into main
5 changed files with 79 additions and 92 deletions

View File

@ -41,6 +41,7 @@
"react-intersection-observer": "^9.5.1",
"react-intl": "^6.4.4",
"react-markdown": "^8.0.7",
"react-player": "^2.13.0",
"react-router-dom": "^6.13.0",
"react-tag-input-component": "^2.0.2",
"semantic-sdp": "^3.26.3",

View File

@ -1,6 +1,5 @@
import Hls from "hls.js";
import { useEffect, useMemo, useRef, useState } from "react";
import { WISH } from "wish";
import { useMemo, useState } from "react";
import ReactPlayer from "react-player";
export enum VideoStatus {
Online = "online",
@ -14,99 +13,29 @@ export interface VideoPlayerProps {
}
export function LiveVideoPlayer(props: VideoPlayerProps) {
const video = useRef<HTMLVideoElement>(null);
const streamCached = useMemo(() => props.stream, [props.stream]);
const [status, setStatus] = useState<VideoStatus>();
const [src, setSrc] = useState<string>();
useEffect(() => {
if (streamCached && video.current) {
if (Hls.isSupported()) {
try {
const hls = new Hls();
hls.loadSource(streamCached);
hls.attachMedia(video.current);
hls.on(Hls.Events.ERROR, (event, data) => {
console.debug(event, data);
const errorType = data.type;
if (errorType === Hls.ErrorTypes.NETWORK_ERROR && data.fatal) {
hls.stopLoad();
hls.detachMedia();
setStatus(VideoStatus.Offline);
}
});
hls.on(Hls.Events.MANIFEST_PARSED, () => {
setStatus(VideoStatus.Online);
});
hls.on(Hls.Events.LEVEL_SWITCHING, (e, l) => {
console.debug("HLS Level Switch", l);
});
return () => hls.destroy();
} catch (e) {
console.error(e);
setStatus(VideoStatus.Offline);
}
} else {
setSrc(streamCached);
setStatus(VideoStatus.Online);
video.current.muted = true;
video.current.load();
}
}
}, [video, streamCached, props.status]);
const [status, setStatus] = useState<VideoStatus>(VideoStatus.Online);
return (
<div className="video-overlay">
<div className={status}>
<div>{status}</div>
</div>
<video
ref={video}
autoPlay={true}
poster={props.poster}
src={src}
playsInline={true}
controls={status === VideoStatus.Online}
<ReactPlayer
controls
playing
url={streamCached}
config={{ file: { attributes: { poster: props.poster } } }}
className="video-player"
width="100%"
height="100%"
onStart={() => setStatus(VideoStatus.Online)}
onError={(type, error) => {
if (error?.type === "networkError") {
setStatus(VideoStatus.Offline);
}
}}
/>
</div>
);
}
export function WebRTCPlayer(props: VideoPlayerProps) {
const video = useRef<HTMLVideoElement>(null);
const streamCached = useMemo(
() => "https://customer-uu10flpvos4pfhgu.cloudflarestream.com/7634aee1af35a2de4ac13ca3d1718a8b/webRTC/play",
[props.stream]
);
const [status] = useState<VideoStatus>();
//https://customer-uu10flpvos4pfhgu.cloudflarestream.com/7634aee1af35a2de4ac13ca3d1718a8b/webRTC/play
useEffect(() => {
if (video.current && streamCached) {
const client = new WISH();
client.addEventListener("log", console.debug);
client.WithEndpoint(streamCached, true);
client
.Play()
.then(s => {
if (video.current) {
video.current.srcObject = s;
}
})
.catch(console.error);
return () => {
client.Disconnect().catch(console.error);
};
}
}, [video, streamCached]);
return (
<div className="video-overlay">
<div className={status}>
<div>{status}</div>
</div>
<video ref={video} autoPlay={true} poster={props.poster} controls={status === VideoStatus.Online} />
</div>
);
}

View File

@ -11,6 +11,7 @@ import { findTag } from "../utils";
import { useLogin } from "hooks/login";
import { NewGoalDialog } from "element/new-goal";
import { useGoals } from "hooks/goals";
import ReactPlayer from "react-player";
export interface StreamEditorProps {
ev?: NostrEvent;
@ -52,6 +53,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
const [summary, setSummary] = useState("");
const [image, setImage] = useState("");
const [stream, setStream] = useState("");
const [record, setRecord] = useState("");
const [status, setStatus] = useState("");
const [start, setStart] = useState<string>();
const [tags, setTags] = useState<string[]>([]);
@ -66,6 +68,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
setSummary(findTag(ev, "summary") ?? "");
setImage(findTag(ev, "image") ?? "");
setStream(findTag(ev, "streaming") ?? "");
setRecord(findTag(ev, "recording") ?? "");
setStatus(findTag(ev, "status") ?? StreamState.Live);
setStart(findTag(ev, "starts"));
setTags(ev?.tags.filter(a => a[0] === "t").map(a => a[1]) ?? []);
@ -80,15 +83,18 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
if (stream.length < 5 || !stream.match(/^https?:\/\/.*\.m3u8?$/i)) {
return false;
}
if (record.length > 0 && !ReactPlayer.canPlay(record)) {
return false;
}
if (image.length > 0 && !image.match(/^https?:\/\//i)) {
return false;
}
return true;
}, [title, image, stream]);
}, [title, image, stream, record]);
useEffect(() => {
setIsValid(ev !== undefined || validate());
}, [validate, title, summary, image, stream]);
}, [ev, validate]);
async function publishStream() {
const pub = login?.publisher();
@ -106,6 +112,9 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
.tag(["streaming", stream])
.tag(["status", status])
.tag(["starts", starts]);
if (record) {
eb.tag(["recording", record]);
}
if (status === StreamState.Ended) {
eb.tag(["ends", ends]);
}
@ -217,6 +226,16 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
</div>
</div>
)}
{status === StreamState.Ended && (
<div>
<p>
<FormattedMessage defaultMessage="Recording URL" />
</p>
<div className="paper">
<input type="text" placeholder="https://" value={record} onChange={e => setRecord(e.target.value)} />
</div>
</div>
)}
</>
)}
{(options?.canSetTags ?? true) && (

View File

@ -20,6 +20,12 @@
.stream-page .video-overlay {
position: relative;
padding-top: 56.25%; /* Player ratio: 100 / (1280 / 720) */
}
.stream-page .video-player {
position: absolute;
inset: 0;
}
.stream-page .video-content video {
@ -124,6 +130,8 @@
position: absolute;
width: 100%;
height: 100%;
top: 0;
z-index: 1;
}
.online > div {

View File

@ -4687,7 +4687,7 @@ __metadata:
languageName: node
linkType: hard
"deepmerge@npm:^4.2.2":
"deepmerge@npm:^4.0.0, deepmerge@npm:^4.2.2":
version: 4.3.1
resolution: "deepmerge@npm:4.3.1"
checksum: 2024c6a980a1b7128084170c4cf56b0fd58a63f2da1660dcfe977415f27b17dbe5888668b59d0b063753f3220719d5e400b7f113609489c90160bb9a5518d052
@ -7066,6 +7066,13 @@ __metadata:
languageName: node
linkType: hard
"load-script@npm:^1.0.0":
version: 1.0.0
resolution: "load-script@npm:1.0.0"
checksum: 8458e3f07b4a86f8d9d66e47a987811491a5d013af23ba7b371c6d3c9dc899885b072ccf65abf7874c10cb197d4975eacd8a7a125bfb38dbbcb267539f5dc1e9
languageName: node
linkType: hard
"loader-runner@npm:^4.2.0":
version: 4.3.0
resolution: "loader-runner@npm:4.3.0"
@ -7328,6 +7335,13 @@ __metadata:
languageName: node
linkType: hard
"memoize-one@npm:^5.1.1":
version: 5.2.1
resolution: "memoize-one@npm:5.2.1"
checksum: a3cba7b824ebcf24cdfcd234aa7f86f3ad6394b8d9be4c96ff756dafb8b51c7f71320785fbc2304f1af48a0467cbbd2a409efc9333025700ed523f254cb52e3d
languageName: node
linkType: hard
"merge-descriptors@npm:1.0.1":
version: 1.0.1
resolution: "merge-descriptors@npm:1.0.1"
@ -8894,7 +8908,7 @@ __metadata:
languageName: node
linkType: hard
"react-fast-compare@npm:^3.1.1":
"react-fast-compare@npm:^3.0.1, react-fast-compare@npm:^3.1.1":
version: 3.2.2
resolution: "react-fast-compare@npm:3.2.2"
checksum: 2071415b4f76a3e6b55c84611c4d24dcb12ffc85811a2840b5a3f1ff2d1a99be1020d9437ee7c6e024c9f4cbb84ceb35e48cf84f28fcb00265ad2dfdd3947704
@ -8995,6 +9009,21 @@ __metadata:
languageName: node
linkType: hard
"react-player@npm:^2.13.0":
version: 2.13.0
resolution: "react-player@npm:2.13.0"
dependencies:
deepmerge: ^4.0.0
load-script: ^1.0.0
memoize-one: ^5.1.1
prop-types: ^15.7.2
react-fast-compare: ^3.0.1
peerDependencies:
react: ">=16.6.0"
checksum: 7e0e69e0ac37227ab5bfdda73991d4f5d4741585562f3ad9cfb787ae2c427510b69ddf6ef3f23f319d699b790af852fca57f3e9b1dae94f385d545a3db200d67
languageName: node
linkType: hard
"react-remove-scroll-bar@npm:^2.3.3":
version: 2.3.4
resolution: "react-remove-scroll-bar@npm:2.3.4"
@ -9981,6 +10010,7 @@ __metadata:
react-intersection-observer: ^9.5.1
react-intl: ^6.4.4
react-markdown: ^8.0.7
react-player: ^2.13.0
react-router-dom: ^6.13.0
react-tag-input-component: ^2.0.2
semantic-sdp: ^3.26.3