feat: Enhance support for a wider variety of video URL formats. #101
@ -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",
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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) && (
|
||||
|
@ -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 {
|
||||
|
34
yarn.lock
34
yarn.lock
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user