Live video on the AT Protocol

Revert "add livepeer transcoding!"

This reverts commit 794c401e1570c816435aa93c3f53d96267213341.

Eli Mallon c823d03c d394c547

+1222 -3154
+2 -4
Makefile
··· 197 197 -D "gst-plugins-base:compositor=enabled" \ 198 198 -D "gst-plugins-base:videorate=enabled" \ 199 199 -D "gst-plugins-base:app=enabled" \ 200 - -D "gst-plugins-base:audiorate=enabled" \ 201 200 -D "gst-plugins-base:audiotestsrc=enabled" \ 202 201 -D "gst-plugins-base:audioconvert=enabled" \ 203 202 -D "gst-plugins-good:matroska=enabled" \ ··· 212 211 -D "gst-plugins-good:audioparsers=enabled" \ 213 212 -D "gst-plugins-bad:videoparsers=enabled" \ 214 213 -D "gst-plugins-bad:mpegtsmux=enabled" \ 215 - -D "gst-plugins-bad:mpegtsdemux=enabled" \ 216 214 -D "gst-plugins-bad:codectimestamper=enabled" \ 217 215 -D "gst-plugins-bad:opus=enabled" \ 218 216 -D "gst-plugins-ugly:x264=enabled" \ 219 217 -D "gst-plugins-ugly:gpl=enabled" \ 220 218 -D "x264:asm=enabled" \ 221 219 -D "gstreamer-full:gst-full=enabled" \ 222 - -D "gstreamer-full:gst-full-plugins=libgstopusparse.a;libgstcodectimestamper.a;libgstrtp.a;libgstaudioresample.a;libgstlibav.a;libgstmatroska.a;libgstmultifile.a;libgstjpeg.a;libgstaudiotestsrc.a;libgstaudioconvert.a;libgstaudioparsers.a;libgstfdkaac.a;libgstisomp4.a;libgstapp.a;libgstvideoconvertscale.a;libgstvideobox.a;libgstvideorate.a;libgstpng.a;libgstcompositor.a;libgstaudiorate.a;libgstx264.a;libgstopus.a;libgstvideotestsrc.a;libgstvideoparsersbad.a;libgstaudioparsers.a;libgstmpegtsmux.a;libgstmpegtsdemux.a;libgstplayback.a;libgsttypefindfunctions.a" \ 220 + -D "gstreamer-full:gst-full-plugins=libgstopusparse.a;libgstcodectimestamper.a;libgstrtp.a;libgstaudioresample.a;libgstlibav.a;libgstmatroska.a;libgstmultifile.a;libgstjpeg.a;libgstaudiotestsrc.a;libgstaudioconvert.a;libgstaudioparsers.a;libgstfdkaac.a;libgstisomp4.a;libgstapp.a;libgstvideoconvertscale.a;libgstvideobox.a;libgstvideorate.a;libgstpng.a;libgstcompositor.a;libgstx264.a;libgstopus.a;libgstvideotestsrc.a;libgstvideoparsersbad.a;libgstaudioparsers.a;libgstmpegtsmux.a;libgstplayback.a;libgsttypefindfunctions.a" \ 223 221 -D "gstreamer-full:gst-full-libraries=gstreamer-controller-1.0,gstreamer-plugins-base-1.0,gstreamer-pbutils-1.0" \ 224 222 -D "gstreamer-full:gst-full-target-type=static_library" \ 225 - -D "gstreamer-full:gst-full-elements=coreelements:concat,filesrc,filesink,queue,queue2,multiqueue,typefind,tee,capsfilter,fakesink,identity" \ 223 + -D "gstreamer-full:gst-full-elements=coreelements:concat,filesrc,queue,queue2,multiqueue,typefind,tee,capsfilter,fakesink,identity" \ 226 224 -D "gstreamer-full:bad=enabled" \ 227 225 -D "gstreamer-full:tls=disabled" \ 228 226 -D "gstreamer-full:libav=enabled" \
+2 -1
js/app/components/livestream/livestream.tsx
··· 30 30 const telemetry = useAppSelector(selectTelemetry); 31 31 const player = useAppSelector(usePlayer()); 32 32 33 - const { src, ...extraProps } = props; 33 + const { src, protocol, ...extraProps } = props; 34 34 const dispatch = useAppDispatch(); 35 35 const { width, height } = useWindowDimensions(); 36 36 const video = player.segment?.video?.[0]; ··· 147 147 <Player 148 148 telemetry={telemetry === true} 149 149 src={src} 150 + forceProtocol={protocol} 150 151 {...extraProps} 151 152 /> 152 153 <View
+29 -117
js/app/components/player/controls.tsx
··· 9 9 Settings, 10 10 Shell, 11 11 Sparkle, 12 + Squirrel, 12 13 Star, 13 14 Volume2, 14 15 VolumeX, 15 16 } from "@tamagui/lucide-icons"; 16 - import { Dispatch, Fragment, useEffect, useRef, useState } from "react"; 17 + import { useEffect, useRef, useState } from "react"; 17 18 import { Animated, Pressable } from "react-native"; 18 19 import { 19 20 Button, ··· 31 32 H5, 32 33 Paragraph, 33 34 } from "tamagui"; 34 - import { PlayerProps, PROTOCOL_HLS, PROTOCOL_WEBRTC } from "./props"; 35 + import { 36 + PlayerProps, 37 + PROTOCOL_HLS, 38 + PROTOCOL_PROGRESSIVE_MP4, 39 + PROTOCOL_PROGRESSIVE_WEBM, 40 + PROTOCOL_WEBRTC, 41 + } from "./props"; 35 42 import { 36 43 usePlayer, 37 44 usePlayerActions, 38 - usePlayerProtocol, 39 - usePlayerRenditions, 40 45 usePlayerSegment, 41 - usePlayerSelectedRendition, 42 46 } from "features/player/playerSlice"; 43 47 import { useAppDispatch, useAppSelector } from "store/hooks"; 44 48 import Loading from "components/loading/loading"; 45 49 import Viewers from "components/viewers"; 46 50 import { userMute } from "features/streamplace/streamplaceSlice"; 47 51 import { Countdown } from "components/countdown"; 48 - import { Rendition } from "lexicons/types/place/stream/defs"; 49 52 50 53 const Bar = (props) => ( 51 54 <XStack ··· 179 182 export function PopoverMenu(props: PlayerProps) { 180 183 const [open, setOpen] = useState(false); 181 184 const media = useMedia(); 182 - const renditions = useAppSelector(usePlayerRenditions()); 183 - const selectedRendition = useAppSelector(usePlayerSelectedRendition()); 184 - const protocol = useAppSelector(usePlayerProtocol()); 185 - const { setSelectedRendition, setProtocol } = usePlayerActions(); 186 - const dispatch = useAppDispatch(); 187 - // on android, this appears to lose its context. idk. so we just pass everything through. 188 - const gearMenu = ( 189 - <GearMenu 190 - {...props} 191 - renditions={renditions} 192 - selectedRendition={selectedRendition ?? "source"} 193 - protocol={protocol} 194 - setSelectedRendition={setSelectedRendition} 195 - setProtocol={setProtocol} 196 - dispatch={dispatch} 197 - /> 198 - ); 199 185 useEffect(() => { 200 186 if (!media.sm && props.showControls === false) { 201 187 setOpen(false); ··· 225 211 226 212 <Adapt when="sm" platform="touch"> 227 213 <Popover.Sheet modal dismissOnSnapToBottom snapPoints={[50]}> 228 - <Popover.Sheet.Frame padding="$2">{gearMenu}</Popover.Sheet.Frame> 214 + <Popover.Sheet.Frame padding="$2"> 215 + <GearMenu {...props} /> 216 + </Popover.Sheet.Frame> 229 217 <Popover.Sheet.Overlay 230 218 animation="lazy" 231 219 enterStyle={{ opacity: 0 }} ··· 250 238 }, 251 239 ]} 252 240 > 253 - {gearMenu} 241 + <GearMenu {...props} /> 254 242 </Popover.Content> 255 243 </Popover> 256 244 ); ··· 308 296 return <Loading />; 309 297 } 310 298 311 - function GearMenu( 312 - props: PlayerProps & { 313 - renditions: Rendition[]; 314 - selectedRendition: string; 315 - protocol: string; 316 - setSelectedRendition: (rendition: string) => void; 317 - setProtocol: (protocol: string) => void; 318 - dispatch: Dispatch<any>; 319 - }, 320 - ) { 299 + function GearMenu(props: PlayerProps) { 321 300 const [menu, setMenu] = useState("root"); 322 - const { 323 - renditions, 324 - selectedRendition, 325 - protocol, 326 - setSelectedRendition, 327 - setProtocol, 328 - dispatch, 329 - } = props; 330 - 331 301 return ( 332 302 <YGroup alignSelf="center" bordered width={240} size="$5" borderRadius="$0"> 333 303 {menu == "root" && ( ··· 349 319 hoverTheme 350 320 pressTheme 351 321 title="Quality" 352 - subTitle="Adjust bandwidth usage" 322 + subTitle="WIP" 353 323 icon={Sparkle} 354 324 iconAfter={ChevronRight} 355 - onPress={() => setMenu("quality")} 356 325 /> 357 326 </YGroup.Item> 358 327 </> ··· 376 345 title="HLS" 377 346 subTitle="HTTP Live Streaming" 378 347 icon={Star} 379 - iconAfter={protocol === PROTOCOL_HLS ? CheckCircle : Circle} 380 - onPress={() => dispatch(setProtocol(PROTOCOL_HLS))} 348 + iconAfter={props.protocol === PROTOCOL_HLS ? CheckCircle : Circle} 349 + onPress={() => props.setProtocol(PROTOCOL_HLS)} 381 350 /> 382 351 </YGroup.Item> 383 - {/* <Separator /> 352 + <Separator /> 384 353 <YGroup.Item> 385 354 <ListItem 386 355 hoverTheme ··· 389 358 subTitle="MP4 but loooong" 390 359 icon={Shell} 391 360 iconAfter={ 392 - protocol === PROTOCOL_PROGRESSIVE_MP4 ? CheckCircle : Circle 361 + props.protocol === PROTOCOL_PROGRESSIVE_MP4 362 + ? CheckCircle 363 + : Circle 393 364 } 394 - onPress={() => dispatch(setProtocol(PROTOCOL_PROGRESSIVE_MP4))} 365 + onPress={() => props.setProtocol(PROTOCOL_PROGRESSIVE_MP4)} 395 366 /> 396 367 </YGroup.Item> 397 368 <Separator /> ··· 403 374 subTitle="WebM but loooong" 404 375 icon={Squirrel} 405 376 iconAfter={ 406 - protocol === PROTOCOL_PROGRESSIVE_WEBM ? CheckCircle : Circle 377 + props.protocol === PROTOCOL_PROGRESSIVE_WEBM 378 + ? CheckCircle 379 + : Circle 407 380 } 408 - onPress={() => dispatch(setProtocol(PROTOCOL_PROGRESSIVE_WEBM))} 381 + onPress={() => props.setProtocol(PROTOCOL_PROGRESSIVE_WEBM)} 409 382 /> 410 - </YGroup.Item> */} 383 + </YGroup.Item> 411 384 <Separator /> 412 385 <YGroup.Item> 413 386 <ListItem ··· 416 389 title="WebRTC" 417 390 subTitle="Lowest latency, probably" 418 391 icon={Antenna} 419 - iconAfter={protocol === PROTOCOL_WEBRTC ? CheckCircle : Circle} 420 - onPress={() => dispatch(setProtocol(PROTOCOL_WEBRTC))} 421 - /> 422 - </YGroup.Item> 423 - </> 424 - )} 425 - {menu == "quality" && ( 426 - <> 427 - <YGroup.Item> 428 - <ListItem 429 - hoverTheme 430 - pressTheme 431 - title="Back" 432 - icon={ChevronLeft} 433 - onPress={() => setMenu("root")} 434 - /> 435 - </YGroup.Item> 436 - <Separator /> 437 - {protocol === PROTOCOL_HLS && ( 438 - <> 439 - <YGroup.Item> 440 - <ListItem 441 - hoverTheme 442 - pressTheme 443 - title="Auto" 444 - subTitle="Automatic with HLS" 445 - icon={Star} 446 - iconAfter={ 447 - props.selectedRendition === "auto" ? CheckCircle : Circle 448 - } 449 - onPress={() => dispatch(setSelectedRendition("auto"))} 450 - /> 451 - </YGroup.Item> 452 - <Separator /> 453 - </> 454 - )} 455 - <YGroup.Item> 456 - <ListItem 457 - hoverTheme 458 - pressTheme 459 - title="Source" 460 - subTitle="Original quality" 461 - icon={Star} 462 392 iconAfter={ 463 - props.selectedRendition === "source" ? CheckCircle : Circle 393 + props.protocol === PROTOCOL_WEBRTC ? CheckCircle : Circle 464 394 } 465 - onPress={() => dispatch(setSelectedRendition("source"))} 395 + onPress={() => props.setProtocol(PROTOCOL_WEBRTC)} 466 396 /> 467 397 </YGroup.Item> 468 - {renditions.map((rendition) => ( 469 - <Fragment key={rendition.name}> 470 - <Separator /> 471 - <YGroup.Item> 472 - <ListItem 473 - hoverTheme 474 - pressTheme 475 - title={rendition.name} 476 - subTitle={rendition.name} 477 - icon={Shell} 478 - iconAfter={ 479 - selectedRendition === rendition.name ? CheckCircle : Circle 480 - } 481 - onPress={() => dispatch(setSelectedRendition(rendition.name))} 482 - /> 483 - </YGroup.Item> 484 - </Fragment> 485 - ))} 486 398 </> 487 399 )} 488 400 </YGroup>
+18 -10
js/app/components/player/player.tsx
··· 10 10 PlayerProps, 11 11 PlayerStatus, 12 12 PlayerStatusTracker, 13 + PROTOCOL_WEBRTC, 13 14 } from "./props"; 14 15 import PlayerProvider from "./provider"; 15 16 import { selectUserMuted } from "features/streamplace/streamplaceSlice"; 16 17 import { useAppSelector } from "store/hooks"; 17 - import { 18 - usePlayerRenditions, 19 - usePlayerSegment, 20 - usePlayerSelectedRendition, 21 - } from "features/player/playerSlice"; 18 + import { usePlayerSegment } from "features/player/playerSlice"; 22 19 23 20 const HIDE_CONTROLS_AFTER = 2000; 24 21 const OFFLINE_THRESHOLD = 10000; ··· 58 55 setTouchTime(Date.now()); 59 56 setShowControls(true); 60 57 }; 58 + // keeping this other logic for now in case we need a second-best choice 59 + let defProto = PROTOCOL_WEBRTC; 60 + // const plat = usePlatform(); 61 + // if (plat.isIOS) { 62 + // defProto = PROTOCOL_HLS; 63 + // } else if (plat.isSafari) { 64 + // defProto = PROTOCOL_HLS; 65 + // } else if (plat.isFirefox) { 66 + // defProto = PROTOCOL_HLS; 67 + // } 68 + if (props.forceProtocol) { 69 + defProto = props.forceProtocol; 70 + } 61 71 const { url } = useStreamplaceNode(); 62 72 const info = usePlatform(); 63 73 const playerEvent = async ( ··· 88 98 }; 89 99 const [status, setStatus] = usePlayerStatus(playerEvent); 90 100 const [playTime, setPlayTime] = useState(0); 101 + const [protocol, setProtocol] = useState(defProto); 91 102 const [fullscreen, setFullscreen] = useState(false); 92 103 93 104 const [offline, setOffline] = useState(true); ··· 96 107 const segment = useAppSelector(usePlayerSegment()); 97 108 const [lastCheck, setLastCheck] = useState(0); 98 109 99 - const renditions = useAppSelector(usePlayerRenditions()); 100 - const selectedRendition = useAppSelector(usePlayerSelectedRendition()); 101 - 102 110 useEffect(() => { 103 111 if (playing) { 104 112 setOffline(false); ··· 135 143 setFullscreen: setFullscreen, 136 144 fullscreen: fullscreen, 137 145 offline: offline, 146 + protocol: protocol, 147 + setProtocol: setProtocol, 138 148 showControls: props.showControls ?? showControls, 139 149 userInteraction: userInteraction, 140 150 playerEvent: playerEvent, ··· 144 154 setPlayTime: setPlayTime, 145 155 ingestMediaSource: props.ingestMediaSource ?? IngestMediaSource.USER, 146 156 ingestAutoStart: props.ingestAutoStart ?? false, 147 - renditions: renditions ?? [], 148 - selectedRendition: selectedRendition ?? "source", 149 157 ...props, 150 158 }; 151 159 return (
+2 -4
js/app/components/player/props.tsx
··· 1 - import { Rendition } from "lexicons/types/place/stream/defs"; 2 - 3 1 export enum IngestMediaSource { 4 2 USER = "user", 5 3 DISPLAY = "display", ··· 11 9 src: string; 12 10 muted: boolean; 13 11 fullscreen: boolean; 12 + protocol: string; 14 13 forceProtocol?: string; 15 14 showControls: boolean; 16 15 telemetry: boolean; 17 16 setMuted: (isMuted: boolean) => void; 18 17 setFullscreen: (isFullscreen: boolean) => void; 18 + setProtocol: (protocol: string) => void; 19 19 userInteraction: () => void; 20 20 playerEvent: ( 21 21 time: string, ··· 33 33 ingestAutoStart?: boolean; 34 34 avSyncTest?: boolean; 35 35 offline: boolean; 36 - renditions: Rendition[]; 37 - selectedRendition: string; 38 36 }; 39 37 40 38 export type PlayerEvent = {
+1 -4
js/app/components/player/provider.tsx
··· 37 37 if (props.playerId) { 38 38 newPlayerAction.payload.playerId = props.playerId; 39 39 } 40 - if (props.forceProtocol) { 41 - newPlayerAction.payload.forceProtocol = props.forceProtocol; 42 - } 40 + setPlayerId(newPlayerAction.payload.playerId); 43 41 dispatch(newPlayerAction); 44 - setPlayerId(newPlayerAction.payload.playerId); 45 42 }, []); 46 43 if (!playerId) { 47 44 return <></>;
+10 -17
js/app/components/player/shared.tsx
··· 15 15 webrtc: PROTOCOL_WEBRTC, 16 16 }; 17 17 18 - export function srcToUrl( 19 - props: PlayerProps, 20 - protocol: string, 21 - ): { 18 + export function srcToUrl(props: PlayerProps): { 22 19 url: string; 23 20 protocol: string; 24 21 } { ··· 37 34 } 38 35 } 39 36 let outUrl; 40 - if (protocol === PROTOCOL_HLS) { 41 - if (props.selectedRendition === "auto") { 42 - outUrl = `${url}/api/playback/${props.src}/hls/index.m3u8`; 43 - } else { 44 - outUrl = `${url}/api/playback/${props.src}/hls/index.m3u8?rendition=${props.selectedRendition}`; 45 - } 46 - } else if (protocol === PROTOCOL_PROGRESSIVE_MP4) { 37 + if (props.protocol === PROTOCOL_HLS) { 38 + outUrl = `${url}/api/playback/${props.src}/hls/stream.m3u8`; 39 + } else if (props.protocol === PROTOCOL_PROGRESSIVE_MP4) { 47 40 outUrl = `${url}/api/playback/${props.src}/stream.mp4`; 48 - } else if (protocol === PROTOCOL_PROGRESSIVE_WEBM) { 41 + } else if (props.protocol === PROTOCOL_PROGRESSIVE_WEBM) { 49 42 outUrl = `${url}/api/playback/${props.src}/stream.webm`; 50 - } else if (protocol === PROTOCOL_WEBRTC) { 51 - outUrl = `${url}/api/playback/${props.src}/webrtc?rendition=${props.selectedRendition}`; 43 + } else if (props.protocol === PROTOCOL_WEBRTC) { 44 + outUrl = `${url}/api/playback/${props.src}/webrtc`; 52 45 } else { 53 - throw new Error(`unknown playback protocol: ${protocol}`); 46 + throw new Error(`unknown playback protocol: ${props.protocol}`); 54 47 } 55 48 return { 56 - protocol: protocol, 49 + protocol: props.protocol, 57 50 url: outUrl, 58 51 }; 59 - }, [props.src, protocol, url]); 52 + }, [props.src, props.protocol, url]); 60 53 }
+1 -1
js/app/components/player/use-webrtc.tsx
··· 126 126 * This specifies how the client should communicate, 127 127 * and what kind of media client and server have negotiated to exchange. 128 128 */ 129 - let response = await postSDPOffer(`${endpoint}`, ofr.sdp, bearerToken); 129 + let response = await postSDPOffer(endpoint, ofr.sdp, bearerToken); 130 130 if (response.status === 201) { 131 131 let answerSDP = await response.text(); 132 132 if ((peerConnection.connectionState as string) === "closed") {
+1 -8
js/app/components/player/video-retry.tsx
··· 1 1 import React, { useEffect, useState } from "react"; 2 2 import { PlayerProps, PlayerStatus } from "./props"; 3 - import { usePlayerSelectedRendition } from "features/player/playerSlice"; 4 - import { useAppSelector } from "store/hooks"; 5 3 6 4 export default function VideoRetry( 7 5 props: PlayerProps & { children: React.ReactNode }, ··· 9 7 const [resetTime, setResetTime] = useState<number>(Date.now()); 10 8 const [retryCount, setRetryCount] = useState(0); 11 9 const isPlaying = props.status === PlayerStatus.PLAYING; 12 - const selectedRendition = useAppSelector(usePlayerSelectedRendition()); 13 10 14 11 useEffect(() => { 15 12 if (isPlaying) { ··· 33 30 return () => clearTimeout(handle); 34 31 }, [isPlaying, resetTime, retryCount]); 35 32 36 - return ( 37 - <React.Fragment key={`${selectedRendition}-${resetTime}`}> 38 - {props.children} 39 - </React.Fragment> 40 - ); 33 + return <React.Fragment key={resetTime}>{props.children}</React.Fragment>; 41 34 }
+3 -6
js/app/components/player/video.native.tsx
··· 6 6 import { srcToUrl } from "./shared"; 7 7 import useWebRTC from "./use-webrtc"; 8 8 import { MediaStream } from "react-native-webrtc"; 9 - import { usePlayerProtocol } from "features/player/playerSlice"; 10 - import { useAppSelector } from "store/hooks"; 11 9 12 10 // export function Player() { 13 11 // return <View f={1}></View>; ··· 16 14 export default function NativeVideo( 17 15 props: PlayerProps & { videoRef: React.RefObject<VideoView> }, 18 16 ) { 19 - const protocol = useAppSelector(usePlayerProtocol()); 20 - if (protocol === PROTOCOL_WEBRTC) { 17 + if (props.protocol === PROTOCOL_WEBRTC) { 21 18 return <NativeWHEP {...props} />; 22 19 } 23 - const { url } = srcToUrl(props, protocol); 20 + const { url } = srcToUrl(props); 24 21 useEffect(() => { 25 22 return () => { 26 23 props.setStatus(PlayerStatus.START); ··· 89 86 } 90 87 91 88 export function NativeWHEP(props: PlayerProps) { 92 - const { url } = srcToUrl(props, PROTOCOL_WEBRTC); 89 + const { url } = srcToUrl(props); 93 90 const [stream, stuck] = useWebRTC(url); 94 91 useEffect(() => { 95 92 if (stuck) {
+4 -6
js/app/components/player/video.tsx
··· 1 1 import streamKey from "components/live-dashboard/stream-key"; 2 2 import { selectStoredKey } from "features/bluesky/blueskySlice"; 3 - import { usePlayer, usePlayerProtocol } from "features/player/playerSlice"; 3 + import { usePlayer } from "features/player/playerSlice"; 4 4 import Hls from "hls.js"; 5 5 import useStreamplaceNode from "hooks/useStreamplaceNode"; 6 6 import { ··· 32 32 export default function WebVideo( 33 33 props: PlayerProps & { videoRef: RefObject<HTMLVideoElement> }, 34 34 ) { 35 - const inProto = useAppSelector(usePlayerProtocol()); 36 - const { url, protocol } = srcToUrl(props, inProto); 35 + const { url, protocol } = srcToUrl(props); 37 36 useEffect(() => { 38 37 if (props.playTime == 0) { 39 38 return; ··· 54 53 } else if (protocol === PROTOCOL_WEBRTC) { 55 54 return <WebRTCPlayer url={url} {...props} />; 56 55 } else { 57 - throw new Error(`unknown playback protocol ${inProto}`); 56 + throw new Error(`unknown playback protocol ${props.protocol}`); 58 57 } 59 58 } 60 59 ··· 193 192 return; 194 193 } 195 194 if (Hls.isSupported()) { 196 - // workaround for not having quite the right number of audio frames :( 197 - var hls = new Hls({ maxAudioFramesDrift: 20 }); 195 + var hls = new Hls(); 198 196 hls.loadSource(props.url); 199 197 try { 200 198 hls.attachMedia(videoRef.current);
+15 -115
js/app/features/player/playerSlice.tsx
··· 1 1 import { AppBskyFeedDefs, AppBskyFeedPost } from "@atproto/api"; 2 2 import { createAction } from "@reduxjs/toolkit"; 3 - import { PROTOCOL_HLS, PROTOCOL_WEBRTC } from "components/player/props"; 4 3 import { StreamplaceState } from "features/streamplace/streamplaceSlice"; 5 4 import { uuidv7 } from "hooks/uuid"; 6 5 import { ··· 10 9 import { createContext, useContext } from "react"; 11 10 import { createAppSlice } from "../../hooks/createSlice"; 12 11 import { Record as ChatMessageRecord } from "../../lexicons/types/place/stream/chat/message"; 13 - import { 14 - BlockView, 15 - isBlockView, 16 - isRenditions, 17 - Rendition, 18 - } from "../../lexicons/types/place/stream/defs"; 12 + import { BlockView, isBlockView } from "../../lexicons/types/place/stream/defs"; 13 + 19 14 import { 20 15 isLivestreamView, 21 16 isViewerCount, ··· 67 62 chatList: MessageViewHydrated[]; 68 63 livestream: LivestreamViewHydrated | null; 69 64 segment: Segment.Record | null; 70 - renditions: Rendition[]; 71 - selectedRendition: string | null; 72 - protocol: string; 73 65 } 74 66 75 67 export interface PlayersState { ··· 80 72 81 73 export const newPlayer = createAction("player/newPlayer", function prepare() { 82 74 return { 83 - payload: { playerId: uuidv7(), forceProtocol: PROTOCOL_WEBRTC }, 75 + payload: { playerId: uuidv7() }, 84 76 }; 85 77 }); 86 78 ··· 142 134 initialState, 143 135 144 136 extraReducers: (builder) => { 145 - builder.addCase( 146 - newPlayer, 147 - ( 148 - state, 149 - action: { payload: { playerId: string; forceProtocol: string } }, 150 - ) => { 151 - state[action.payload.playerId] = { 152 - ingestStarted: null, 153 - ingestStarting: false, 154 - ingestConnectionState: null, 155 - viewers: null, 156 - protocol: action.payload.forceProtocol ?? PROTOCOL_WEBRTC, 157 - chat: {}, 158 - chatList: [], 159 - livestream: null, 160 - segment: null, 161 - renditions: [], 162 - selectedRendition: "source", 163 - }; 164 - }, 165 - ); 137 + builder.addCase(newPlayer, (state, action) => { 138 + state[action.payload.playerId] = { 139 + ingestStarted: null, 140 + ingestStarting: false, 141 + ingestConnectionState: null, 142 + viewers: null, 143 + chat: {}, 144 + chatList: [], 145 + livestream: null, 146 + segment: null, 147 + }; 148 + }); 166 149 }, 167 150 168 151 reducers: (create) => { ··· 257 240 [], 258 241 [block], 259 242 ), 260 - }; 261 - } else if (isRenditions(message)) { 262 - state = { 263 - ...state, 264 - [action.payload.playerId]: { 265 - ...state[action.payload.playerId], 266 - renditions: message.renditions, 267 - }, 268 243 }; 269 244 } 270 245 } ··· 402 377 }, 403 378 }, 404 379 ), 405 - 406 - setSelectedRendition: create.reducer( 407 - ( 408 - state, 409 - action: { 410 - payload: { playerId: string; rendition: string }; 411 - type: string; 412 - }, 413 - ) => { 414 - return { 415 - ...state, 416 - [action.payload.playerId]: { 417 - ...state[action.payload.playerId], 418 - selectedRendition: action.payload.rendition, 419 - }, 420 - }; 421 - }, 422 - ), 423 - 424 - setProtocol: create.reducer( 425 - ( 426 - state, 427 - action: { 428 - payload: { playerId: string; protocol: string }; 429 - type: string; 430 - }, 431 - ) => { 432 - const newPlayer = { 433 - ...state[action.payload.playerId], 434 - protocol: action.payload.protocol, 435 - }; 436 - if (action.payload.protocol === PROTOCOL_HLS) { 437 - newPlayer.selectedRendition = "auto"; 438 - } else { 439 - if (newPlayer.selectedRendition === "auto") { 440 - newPlayer.selectedRendition = "source"; 441 - } 442 - } 443 - return { 444 - ...state, 445 - [action.payload.playerId]: newPlayer, 446 - }; 447 - }, 448 - ), 449 380 }; 450 381 }, 451 382 ··· 462 393 selectSegment: (state, playerId: string) => { 463 394 return state[playerId].segment; 464 395 }, 465 - selectRenditions: (state, playerId: string) => { 466 - return state[playerId].renditions; 467 - }, 468 - selectSelectedRendition: (state, playerId: string) => { 469 - return state[playerId].selectedRendition; 470 - }, 471 - selectProtocol: (state, playerId: string) => { 472 - return state[playerId].protocol; 473 - }, 474 396 }, 475 397 }); 476 398 ··· 496 418 playerSlice.actions.pollSegment({ playerId, user }), 497 419 handleWebSocketMessages: (messages: any[]) => 498 420 playerSlice.actions.handleWebSocketMessages({ playerId, messages }), 499 - setSelectedRendition: (rendition: string) => 500 - playerSlice.actions.setSelectedRendition({ playerId, rendition }), 501 - setProtocol: (protocol: string) => 502 - playerSlice.actions.setProtocol({ playerId, protocol }), 503 421 }; 504 422 }; 505 423 ··· 530 448 const playerId = usePlayerId(); 531 449 return (state) => state.player[playerId].segment; 532 450 }; 533 - export const usePlayerRenditions = (): ((state: { 534 - player: PlayersState; 535 - }) => Rendition[]) => { 536 - const playerId = usePlayerId(); 537 - return (state) => state.player[playerId].renditions; 538 - }; 539 - export const usePlayerSelectedRendition = (): ((state: { 540 - player: PlayersState; 541 - }) => string | null) => { 542 - const playerId = usePlayerId(); 543 - return (state) => state.player[playerId].selectedRendition; 544 - }; 545 - export const usePlayerProtocol = (): ((state: { 546 - player: PlayersState; 547 - }) => string) => { 548 - const playerId = usePlayerId(); 549 - return (state) => state.player[playerId].protocol; 550 - };
+1 -1
js/desktop/package.json
··· 5 5 "description": "Streamplace Desktop Application", 6 6 "main": ".webpack/main", 7 7 "scripts": { 8 - "start": "PORT=38082 electron-forge start \"$@\" | cat", 8 + "start": "electron-forge start \"$@\" | cat", 9 9 "start-with-node": "electron-forge start \"$@\" | cat", 10 10 "package": "electron-forge package", 11 11 "make": "electron-forge make",
-17
lexicons/place/stream/defs.json
··· 15 15 "record": { "type": "ref", "ref": "app.bsky.graph.block" }, 16 16 "indexedAt": { "type": "string", "format": "datetime" } 17 17 } 18 - }, 19 - "renditions": { 20 - "type": "object", 21 - "required": ["renditions"], 22 - "properties": { 23 - "renditions": { 24 - "type": "array", 25 - "items": { "type": "ref", "ref": "#rendition" } 26 - } 27 - } 28 - }, 29 - "rendition": { 30 - "type": "object", 31 - "required": ["name"], 32 - "properties": { 33 - "name": { "type": "string" } 34 - } 35 18 } 36 19 } 37 20 }
-2
lexicons/place/stream/livestream.json
··· 65 65 "#livestreamView", 66 66 "#viewerCount", 67 67 "place.stream.defs#blockView", 68 - "place.stream.defs#renditions", 69 - "place.stream.defs#rendition", 70 68 "place.stream.chat.defs#messageView" 71 69 ] 72 70 }
+1 -17
lexicons/place/stream/segment.json
··· 23 23 "format": "datetime", 24 24 "description": "When this segment started" 25 25 }, 26 - "duration": { 27 - "type": "integer", 28 - "description": "The duration of the segment in nanoseconds" 29 - }, 30 26 "creator": { 31 27 "type": "string", 32 28 "format": "did" ··· 63 59 "properties": { 64 60 "codec": { "type": "string", "enum": ["h264"] }, 65 61 "width": { "type": "integer" }, 66 - "height": { "type": "integer" }, 67 - "framerate": { 68 - "type": "ref", 69 - "ref": "#framerate" 70 - } 71 - } 72 - }, 73 - "framerate": { 74 - "type": "object", 75 - "required": ["num", "den"], 76 - "properties": { 77 - "num": { "type": "integer" }, 78 - "den": { "type": "integer" } 62 + "height": { "type": "integer" } 79 63 } 80 64 } 81 65 }
+6 -34
pkg/api/api.go
··· 28 28 "stream.place/streamplace/pkg/bus" 29 29 "stream.place/streamplace/pkg/config" 30 30 "stream.place/streamplace/pkg/crypto/signers/eip712" 31 - "stream.place/streamplace/pkg/director" 32 31 apierrors "stream.place/streamplace/pkg/errors" 33 32 "stream.place/streamplace/pkg/linking" 34 33 "stream.place/streamplace/pkg/log" ··· 36 35 "stream.place/streamplace/pkg/mist/mistconfig" 37 36 "stream.place/streamplace/pkg/model" 38 37 "stream.place/streamplace/pkg/notifications" 39 - "stream.place/streamplace/pkg/renditions" 40 38 "stream.place/streamplace/pkg/spmetrics" 41 39 "stream.place/streamplace/pkg/streamplace" 42 40 ) ··· 51 49 MediaManager *media.MediaManager 52 50 MediaSigner media.MediaSigner 53 51 // not thread-safe yet 54 - Aliases map[string]string 55 - Bus *bus.Bus 56 - ATSync *atproto.ATProtoSynchronizer 57 - Director *director.Director 52 + Aliases map[string]string 53 + Bus *bus.Bus 54 + ATSync *atproto.ATProtoSynchronizer 58 55 } 59 56 60 - func MakeStreamplaceAPI(cli *config.CLI, mod model.Model, signer *eip712.EIP712Signer, noter notifications.FirebaseNotifier, mm *media.MediaManager, ms media.MediaSigner, bus *bus.Bus, atsync *atproto.ATProtoSynchronizer, d *director.Director) (*StreamplaceAPI, error) { 57 + func MakeStreamplaceAPI(cli *config.CLI, mod model.Model, signer *eip712.EIP712Signer, noter notifications.FirebaseNotifier, mm *media.MediaManager, ms media.MediaSigner, bus *bus.Bus, atsync *atproto.ATProtoSynchronizer) (*StreamplaceAPI, error) { 61 58 updater, err := PrepareUpdater(cli) 62 59 if err != nil { 63 60 return nil, err ··· 72 69 Aliases: map[string]string{}, 73 70 Bus: bus, 74 71 ATSync: atsync, 75 - Director: d, 76 72 } 77 73 a.Mimes, err = updater.GetMimes() 78 74 if err != nil { ··· 99 95 return nil, ErrorIndex 100 96 } 101 97 102 - // api/playback/iame.li/webrtc?rendition=source 103 - // api/playback/iame.li/stream.mp4?rendition=source 104 - // api/playback/iame.li/stream.webm?rendition=source 105 - // api/playback/iame.li/hls/index.m3u8 106 - // api/playback/iame.li/hls/source/stream.m3u8 107 - // api/playback/iame.li/hls/source/000000000000.ts 108 - 109 98 func (a *StreamplaceAPI) Handler(ctx context.Context) (http.Handler, error) { 110 99 router := httprouter.New() 111 100 apiRouter := httprouter.New() ··· 118 107 apiRouter.POST("/api/webrtc/:stream", a.MistProxyHandler(ctx, "/webrtc/%s")) 119 108 apiRouter.OPTIONS("/api/webrtc/:stream", a.MistProxyHandler(ctx, "/webrtc/%s")) 120 109 apiRouter.DELETE("/api/webrtc/:stream", a.MistProxyHandler(ctx, "/webrtc/%s")) 110 + apiRouter.GET("/api/hls/:stream/*resource", a.MistProxyHandler(ctx, "/hls/%s")) 121 111 apiRouter.Handler("POST", "/api/segment", a.HandleSegment(ctx)) 122 112 apiRouter.HandlerFunc("GET", "/api/healthz", a.HandleHealthz(ctx)) 123 - apiRouter.GET("/api/playback/:user/hls/*file", a.HandleHLSPlayback(ctx)) 124 113 apiRouter.GET("/api/playback/:user/stream.mp4", a.HandleMP4Playback(ctx)) 125 114 apiRouter.GET("/api/playback/:user/stream.webm", a.HandleMKVPlayback(ctx)) 115 + apiRouter.GET("/api/playback/:user/hls/:file", a.HandleHLSPlayback(ctx)) 126 116 // they're, uh, not jpegs. but we used this once and i don't wanna break backwards compatibility 127 117 apiRouter.GET("/api/playback/:user/stream.jpg", a.HandleThumbnailPlayback(ctx)) 128 118 // this one is not a lie ··· 746 736 return 747 737 } 748 738 initialBurst <- spSeg 749 - if a.CLI.LivepeerGatewayURL != "" { 750 - renditions, err := renditions.GenerateRenditions(spSeg) 751 - if err != nil { 752 - log.Error(ctx, "could not generate renditions", "error", err) 753 - return 754 - } 755 - outRs := streamplace.Defs_Renditions{ 756 - LexiconTypeID: "place.stream.defs#renditions", 757 - } 758 - outRs.Renditions = []*streamplace.Defs_Rendition{} 759 - for _, r := range renditions { 760 - outRs.Renditions = append(outRs.Renditions, &streamplace.Defs_Rendition{ 761 - LexiconTypeID: "place.stream.defs#rendition", 762 - Name: r.Name, 763 - }) 764 - } 765 - initialBurst <- outRs 766 - } 767 739 }() 768 740 769 741 go func() {
+12 -84
pkg/api/api_internal.go
··· 29 29 "stream.place/streamplace/pkg/mist/misttriggers" 30 30 "stream.place/streamplace/pkg/model" 31 31 notificationpkg "stream.place/streamplace/pkg/notifications" 32 - "stream.place/streamplace/pkg/renditions" 33 32 v0 "stream.place/streamplace/pkg/schema/v0" 34 33 ) 35 34 ··· 80 79 router.Handler("GET", "/debug/pprof/heap", pprof.Handler("heap")) 81 80 router.Handler("GET", "/debug/pprof/threadcreate", pprof.Handler("threadcreate")) 82 81 router.Handler("GET", "/debug/pprof/block", pprof.Handler("block")) 83 - router.Handler("GET", "/debug/pprof/allocs", pprof.Handler("allocs")) 84 - router.Handler("GET", "/debug/pprof/mutex", pprof.Handler("mutex")) 85 82 86 83 router.POST("/gc", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 87 84 runtime.GC() ··· 90 87 91 88 router.Handler("GET", "/metrics", promhttp.Handler()) 92 89 93 - router.GET("/playback/:user/:rendition/concat", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 90 + router.GET("/playback/:user/concat", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 94 91 user := p.ByName("user") 95 92 if user == "" { 96 93 errors.WriteHTTPBadRequest(w, "user required", nil) 97 94 return 98 95 } 99 - rendition := p.ByName("rendition") 100 - if rendition == "" { 101 - errors.WriteHTTPBadRequest(w, "rendition required", nil) 102 - return 103 - } 104 96 user, err := a.NormalizeUser(ctx, user) 105 97 if err != nil { 106 98 errors.WriteHTTPBadRequest(w, "invalid user", err) ··· 110 102 fmt.Fprintf(w, "ffconcat version 1.0\n") 111 103 // intermittent reports that you need two here to make things work properly? shouldn't matter. 112 104 for i := 0; i < 2; i += 1 { 113 - fmt.Fprintf(w, "file '%s/playback/%s/%s/latest.mp4'\n", a.CLI.OwnInternalURL(), user, rendition) 105 + fmt.Fprintf(w, "file '%s/playback/%s/latest.mp4'\n", a.CLI.OwnInternalURL(), user) 114 106 } 115 107 }) 116 108 117 - router.GET("/playback/:user/:rendition/latest.mp4", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 109 + router.GET("/playback/:user/latest.mp4", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 118 110 user := p.ByName("user") 119 111 if user == "" { 120 112 errors.WriteHTTPBadRequest(w, "user required", nil) ··· 125 117 errors.WriteHTTPBadRequest(w, "invalid user", err) 126 118 return 127 119 } 128 - rendition := p.ByName("rendition") 129 - if rendition == "" { 130 - errors.WriteHTTPBadRequest(w, "rendition required", nil) 131 - return 132 - } 133 - seg := <-a.MediaManager.SubscribeSegment(ctx, user, rendition) 134 - base := filepath.Base(seg.Filepath) 135 - w.Header().Set("Location", fmt.Sprintf("%s/playback/%s/%s/segment/%s\n", a.CLI.OwnInternalURL(), user, rendition, base)) 120 + file := <-a.MediaManager.SubscribeSegment(ctx, user) 121 + base := filepath.Base(file) 122 + w.Header().Set("Location", fmt.Sprintf("%s/playback/%s/segment/%s\n", a.CLI.OwnInternalURL(), user, base)) 136 123 w.WriteHeader(301) 137 124 }) 138 125 139 - router.GET("/playback/:user/:rendition/segment/:file", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 126 + router.GET("/playback/:user/segment/:file", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 140 127 user := p.ByName("user") 141 128 if user == "" { 142 129 errors.WriteHTTPBadRequest(w, "user required", nil) ··· 160 147 http.ServeFile(w, r, fullpath) 161 148 }) 162 149 163 - router.GET("/playback/:user/:rendition/stream.mkv", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 150 + router.GET("/playback/:user/stream.mkv", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 164 151 user := p.ByName("user") 165 152 if user == "" { 166 153 errors.WriteHTTPBadRequest(w, "user required", nil) 167 154 return 168 155 } 169 - rendition := p.ByName("rendition") 170 - if rendition == "" { 171 - errors.WriteHTTPBadRequest(w, "rendition required", nil) 172 - return 173 - } 174 156 user, err := a.NormalizeUser(ctx, user) 175 157 if err != nil { 176 158 errors.WriteHTTPBadRequest(w, "invalid user", err) ··· 178 160 } 179 161 w.Header().Set("Content-Type", "video/x-matroska") 180 162 w.WriteHeader(200) 181 - err = a.MediaManager.SegmentToMKVPlusOpus(ctx, user, rendition, w) 163 + err = a.MediaManager.SegmentToMKVPlusOpus(ctx, user, w) 182 164 if err != nil { 183 165 log.Log(ctx, "stream.mkv error", "error", err) 184 166 } 185 167 }) 186 168 187 - router.GET("/playback/:user/:rendition/stream.mp4", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 169 + router.GET("/playback/:user/stream.mp4", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 188 170 user := p.ByName("user") 189 171 if user == "" { 190 172 errors.WriteHTTPBadRequest(w, "user required", nil) 191 - return 192 - } 193 - rendition := p.ByName("rendition") 194 - if rendition == "" { 195 - errors.WriteHTTPBadRequest(w, "rendition required", nil) 196 173 return 197 174 } 198 175 user, err := a.NormalizeUser(ctx, user) ··· 220 197 pr, pw := io.Pipe() 221 198 bufw := bufio.NewWriter(pw) 222 199 g.Go(func() error { 223 - return a.MediaManager.SegmentToMP4(ctx, user, rendition, bufw) 200 + return a.MediaManager.SegmentToMP4(ctx, user, bufw) 224 201 }) 225 202 g.Go(func() error { 226 203 time.Sleep(time.Duration(delayMS) * time.Millisecond) ··· 230 207 g.Wait() 231 208 }) 232 209 233 - router.HEAD("/playback/:user/:rendition/stream.mkv", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 210 + router.HEAD("/playback/:user/stream.mkv", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 234 211 user := p.ByName("user") 235 212 if user == "" { 236 213 errors.WriteHTTPBadRequest(w, "user required", nil) ··· 474 451 } 475 452 476 453 w.WriteHeader(http.StatusNoContent) 477 - }) 478 - 479 - router.POST("/livepeer-auth-webhook-url", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { 480 - var payload struct { 481 - URL string `json:"url"` 482 - } 483 - // urls look like http://127.0.0.1:9999/live/did:plc:dkh4rwafdcda4ko7lewe43ml-uucbv40mdkcfat50/47.mp4 484 - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { 485 - errors.WriteHTTPBadRequest(w, "invalid request body (could not decode)", err) 486 - return 487 - } 488 - parts := strings.Split(payload.URL, "/") 489 - if len(parts) < 5 { 490 - errors.WriteHTTPBadRequest(w, "invalid request body (too few parts)", nil) 491 - return 492 - } 493 - didSession := parts[4] 494 - idParts := strings.Split(didSession, "-") 495 - if len(idParts) != 2 { 496 - errors.WriteHTTPBadRequest(w, "invalid request body (invalid did session)", nil) 497 - return 498 - } 499 - did := idParts[0] 500 - // sessionID := idParts[1] 501 - seg, err := a.Model.LatestSegmentForUser(did) 502 - if err != nil { 503 - errors.WriteHTTPInternalServerError(w, "unable to get latest segment", err) 504 - return 505 - } 506 - spseg, err := seg.ToStreamplaceSegment() 507 - if err != nil { 508 - errors.WriteHTTPInternalServerError(w, "unable to convert segment to streamplace segment", err) 509 - return 510 - } 511 - renditions, err := renditions.GenerateRenditions(spseg) 512 - if err != nil { 513 - errors.WriteHTTPInternalServerError(w, "unable to generate renditions", err) 514 - return 515 - } 516 - out := map[string]any{ 517 - "manifestID": didSession, 518 - "profiles": renditions.ToLivepeerProfiles(), 519 - } 520 - bs, err := json.Marshal(out) 521 - if err != nil { 522 - errors.WriteHTTPInternalServerError(w, "unable to marshal json", err) 523 - return 524 - } 525 - w.Write(bs) 526 454 }) 527 455 528 456 handler := sloghttp.Recovery(router)
-12
pkg/api/api_util.go
··· 1 - package api 2 - 3 - import "net/http" 4 - 5 - // get rendition from query params, defaulting to "source" 6 - func getRendition(r *http.Request) string { 7 - rendition := r.URL.Query().Get("rendition") 8 - if rendition == "" { 9 - rendition = "source" 10 - } 11 - return rendition 12 - }
+8 -12
pkg/api/playback.go
··· 52 52 errors.WriteHTTPBadRequest(w, "user required", nil) 53 53 return 54 54 } 55 - rendition := getRendition(r) 56 55 user, err := a.NormalizeUser(ctx, user) 57 56 if err != nil { 58 57 errors.WriteHTTPBadRequest(w, "invalid user", err) ··· 80 79 pr, pw := io.Pipe() 81 80 bufw := bufio.NewWriter(pw) 82 81 g.Go(func() error { 83 - return a.MediaManager.SegmentToMP4(ctx, user, rendition, bufw) 82 + return a.MediaManager.SegmentToMP4(ctx, user, bufw) 84 83 }) 85 84 g.Go(func() error { 86 85 <-ctx.Done() ··· 104 103 errors.WriteHTTPBadRequest(w, "user required", nil) 105 104 return 106 105 } 107 - rendition := getRendition(r) 108 106 user, err := a.NormalizeUser(ctx, user) 109 107 if err != nil { 110 108 errors.WriteHTTPBadRequest(w, "invalid user", err) ··· 132 130 pr, pw := io.Pipe() 133 131 bufw := bufio.NewWriter(pw) 134 132 g.Go(func() error { 135 - return a.MediaManager.SegmentToMKV(ctx, user, rendition, bufw) 133 + return a.MediaManager.SegmentToMKV(ctx, user, bufw) 136 134 }) 137 135 g.Go(func() error { 138 136 <-ctx.Done() ··· 156 154 errors.WriteHTTPBadRequest(w, "user required", nil) 157 155 return 158 156 } 159 - rendition := getRendition(r) 160 157 user, err := a.NormalizeUser(ctx, user) 161 158 if err != nil { 162 159 errors.WriteHTTPBadRequest(w, "invalid user", err) ··· 168 165 return 169 166 } 170 167 offer := webrtc.SessionDescription{Type: webrtc.SDPTypeOffer, SDP: string(body)} 171 - answer, err := a.MediaManager.WebRTCPlayback(ctx, user, rendition, &offer) 168 + answer, err := a.MediaManager.WebRTCPlayback(ctx, user, &offer) 172 169 if err != nil { 173 170 errors.WriteHTTPInternalServerError(w, "error playing back", err) 174 171 return ··· 349 346 errors.WriteHTTPBadRequest(w, "file required", nil) 350 347 return 351 348 } 352 - m3u8, err := a.Director.GetM3U8(ctx, user) 349 + m3u8, err := a.MediaManager.SegmentToHLSOnce(ctx, user) 353 350 if err != nil { 354 - errors.WriteHTTPNotFound(w, "could not get m3u8", err) 351 + errors.WriteHTTPInternalServerError(w, "SegmentToHLSOnce failed", nil) 355 352 return 356 353 } 357 354 session := r.URL.Query().Get("session") 358 - rendition := r.URL.Query().Get("rendition") 359 - buf, err := m3u8.GetFile(file, session, rendition) 355 + buf, err := m3u8.GetSegment(file, session) 360 356 if err != nil { 361 357 errors.WriteHTTPNotFound(w, "segment not found", err) 362 358 return 363 359 } 364 360 365 361 if strings.HasSuffix(file, ".m3u8") { 366 - w.Header().Set("Content-Type", "application/vnd.apple.mpegurl") 362 + w.Header().Set("Content-Type", "application/x-mpegURL") 367 363 } else { 368 364 if session != "" { 369 365 spmetrics.SessionSeen(user, session) 370 366 } 371 - w.Header().Set("Content-Type", "video/mp2t") 367 + w.Header().Set("Content-Type", "video/MP2T") 372 368 } 373 369 374 370 http.ServeContent(w, r, file, time.Now(), bytes.NewReader(buf))
+65 -6
pkg/cmd/streamplace.go
··· 1 1 package cmd 2 2 3 3 import ( 4 + "bytes" 4 5 "context" 5 6 "crypto" 6 7 "flag" ··· 12 13 "runtime/pprof" 13 14 "strconv" 14 15 "syscall" 16 + "time" 15 17 16 18 "golang.org/x/term" 17 19 "stream.place/streamplace/pkg/aqhttp" 20 + "stream.place/streamplace/pkg/aqtime" 18 21 "stream.place/streamplace/pkg/atproto" 19 22 "stream.place/streamplace/pkg/bus" 20 23 "stream.place/streamplace/pkg/crypto/signers" 21 24 "stream.place/streamplace/pkg/crypto/signers/eip712" 22 - "stream.place/streamplace/pkg/director" 23 25 "stream.place/streamplace/pkg/log" 24 26 "stream.place/streamplace/pkg/media" 25 27 "stream.place/streamplace/pkg/notifications" ··· 27 29 "stream.place/streamplace/pkg/replication/boring" 28 30 v0 "stream.place/streamplace/pkg/schema/v0" 29 31 "stream.place/streamplace/pkg/spmetrics" 32 + "stream.place/streamplace/pkg/thumbnail" 30 33 31 34 "github.com/ThalesGroup/crypto11" 32 35 _ "github.com/go-gst/go-glib/glib" ··· 128 131 fs.StringVar(&cli.AppBundleID, "app-bundle-id", "", "bundle id of an app that we facilitate oauth login for") 129 132 fs.StringVar(&cli.StreamerName, "streamer-name", "", "name of the person streaming from this streamplace node") 130 133 fs.StringVar(&cli.FrontendProxy, "dev-frontend-proxy", "", "(FOR DEVELOPMENT ONLY) proxy frontend requests to this address instead of using the bundled frontend") 131 - fs.StringVar(&cli.LivepeerGatewayURL, "livepeer-gateway-url", "", "URL of the Livepeer Gateway to use for transcoding") 132 134 fs.BoolVar(&cli.WideOpen, "wide-open", false, "allow ALL streams to be uploaded to this node (not recommended for production)") 133 135 cli.StringSliceFlag(fs, &cli.AllowedStreams, "allowed-streams", "", "if set, only allow these addresses or atproto DIDs to upload to this node") 134 136 cli.StringSliceFlag(fs, &cli.Peers, "peers", "", "other streamplace nodes to replicate to") ··· 297 299 return err 298 300 } 299 301 300 - d := director.NewDirector(mm, mod, &cli, b) 301 - 302 - a, err := api.MakeStreamplaceAPI(&cli, mod, eip712signer, noter, mm, ms, b, atsync, d) 302 + a, err := api.MakeStreamplaceAPI(&cli, mod, eip712signer, noter, mm, ms, b, atsync) 303 303 if err != nil { 304 304 return err 305 305 } ··· 339 339 }) 340 340 341 341 group.Go(func() error { 342 - return d.Start(ctx) 342 + newSeg := mm.NewSegment() 343 + for { 344 + select { 345 + case <-ctx.Done(): 346 + return nil 347 + case not := <-newSeg: 348 + err := mod.CreateSegment(not.Segment) 349 + if err != nil { 350 + log.Error(ctx, "could not add segment to database", "error", err) 351 + } 352 + spseg, err := not.Segment.ToStreamplaceSegment() 353 + if err != nil { 354 + log.Error(ctx, "could not convert segment to streamplace segment", "error", err) 355 + continue 356 + } 357 + b.Publish(spseg.Creator, spseg) 358 + go func() { 359 + err := func() error { 360 + lock := thumbnail.GetThumbnailLock(not.Segment.RepoDID) 361 + locked := lock.TryLock() 362 + if !locked { 363 + // we're already generating a thumbnail for this user, skip 364 + return nil 365 + } 366 + defer lock.Unlock() 367 + oldThumb, err := mod.LatestThumbnailForUser(not.Segment.RepoDID) 368 + if err != nil { 369 + return err 370 + } 371 + if oldThumb != nil && not.Segment.StartTime.Sub(oldThumb.Segment.StartTime) < time.Minute { 372 + // we have a thumbnail <60sec old, skip generating a new one 373 + return nil 374 + } 375 + r := bytes.NewReader(not.Data) 376 + aqt := aqtime.FromTime(not.Segment.StartTime) 377 + fd, err := cli.SegmentFileCreate(not.Segment.RepoDID, aqt, "jpg") 378 + if err != nil { 379 + return err 380 + } 381 + defer fd.Close() 382 + err = mm.Thumbnail(ctx, r, fd) 383 + if err != nil { 384 + return err 385 + } 386 + thumb := &model.Thumbnail{ 387 + Format: "jpg", 388 + SegmentID: not.Segment.ID, 389 + } 390 + err = mod.CreateThumbnail(thumb) 391 + if err != nil { 392 + return err 393 + } 394 + return nil 395 + }() 396 + if err != nil { 397 + log.Error(ctx, "could not create thumbnail", "error", err) 398 + } 399 + }() 400 + } 401 + } 343 402 }) 344 403 345 404 if cli.TestStream {
-1
pkg/config/config.go
··· 84 84 NoFirehose bool 85 85 PrintChat bool 86 86 Color string 87 - LivepeerGatewayURL string 88 87 89 88 dataDirFlags []*string 90 89 }
-95
pkg/director/director.go
··· 1 - package director 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "sync" 7 - 8 - "golang.org/x/sync/errgroup" 9 - "stream.place/streamplace/pkg/bus" 10 - "stream.place/streamplace/pkg/config" 11 - "stream.place/streamplace/pkg/log" 12 - "stream.place/streamplace/pkg/media" 13 - "stream.place/streamplace/pkg/model" 14 - ) 15 - 16 - // director is responsible for managing the lifecycle of a stream, making business 17 - // logic decisions about when to do things like 18 - // - size of the in-memory segment cache 19 - // - transcoding 20 - // - thumbnail generation 21 - 22 - type Director struct { 23 - mm *media.MediaManager 24 - mod model.Model 25 - cli *config.CLI 26 - bus *bus.Bus 27 - streamSessions map[string]*StreamSession 28 - streamSessionsMu sync.Mutex 29 - } 30 - 31 - func NewDirector(mm *media.MediaManager, mod model.Model, cli *config.CLI, bus *bus.Bus) *Director { 32 - return &Director{ 33 - mm: mm, 34 - mod: mod, 35 - cli: cli, 36 - bus: bus, 37 - streamSessions: make(map[string]*StreamSession), 38 - streamSessionsMu: sync.Mutex{}, 39 - } 40 - } 41 - 42 - func (d *Director) Start(ctx context.Context) error { 43 - newSeg := d.mm.NewSegment() 44 - ctx, cancel := context.WithCancel(ctx) 45 - defer cancel() 46 - g, ctx := errgroup.WithContext(ctx) 47 - for { 48 - select { 49 - case <-ctx.Done(): 50 - cancel() 51 - return g.Wait() 52 - case not := <-newSeg: 53 - d.streamSessionsMu.Lock() 54 - ss, ok := d.streamSessions[not.Segment.RepoDID] 55 - if !ok { 56 - ss = &StreamSession{ 57 - hls: nil, 58 - lp: nil, 59 - repoDID: not.Segment.RepoDID, 60 - mm: d.mm, 61 - mod: d.mod, 62 - cli: d.cli, 63 - bus: d.bus, 64 - segmentChan: make(chan struct{}), 65 - } 66 - d.streamSessions[not.Segment.RepoDID] = ss 67 - g.Go(func() error { 68 - err := ss.Start(ctx, not) 69 - if err != nil { 70 - log.Error(ctx, "could not start stream session", "error", err) 71 - } 72 - d.streamSessionsMu.Lock() 73 - delete(d.streamSessions, not.Segment.RepoDID) 74 - d.streamSessionsMu.Unlock() 75 - return nil 76 - }) 77 - } 78 - d.streamSessionsMu.Unlock() 79 - err := ss.NewSegment(ctx, not) 80 - if err != nil { 81 - log.Error(ctx, "could not add segment to stream session", "error", err) 82 - } 83 - } 84 - } 85 - } 86 - 87 - func (d *Director) GetM3U8(ctx context.Context, repoDID string) (*media.M3U8, error) { 88 - d.streamSessionsMu.Lock() 89 - defer d.streamSessionsMu.Unlock() 90 - ss, ok := d.streamSessions[repoDID] 91 - if !ok { 92 - return nil, fmt.Errorf("stream session not found") 93 - } 94 - return ss.hls, nil 95 - }
-241
pkg/director/stream_session.go
··· 1 - package director 2 - 3 - import ( 4 - "bytes" 5 - "context" 6 - "fmt" 7 - "time" 8 - 9 - "golang.org/x/sync/errgroup" 10 - "stream.place/streamplace/pkg/aqtime" 11 - "stream.place/streamplace/pkg/bus" 12 - "stream.place/streamplace/pkg/config" 13 - "stream.place/streamplace/pkg/livepeer" 14 - "stream.place/streamplace/pkg/log" 15 - "stream.place/streamplace/pkg/media" 16 - "stream.place/streamplace/pkg/media/segchanman" 17 - "stream.place/streamplace/pkg/model" 18 - "stream.place/streamplace/pkg/renditions" 19 - "stream.place/streamplace/pkg/streamplace" 20 - "stream.place/streamplace/pkg/thumbnail" 21 - ) 22 - 23 - type StreamSession struct { 24 - mm *media.MediaManager 25 - mod model.Model 26 - cli *config.CLI 27 - bus *bus.Bus 28 - hls *media.M3U8 29 - lp *livepeer.LivepeerSession 30 - repoDID string 31 - segmentChan chan struct{} 32 - } 33 - 34 - func (ss *StreamSession) Start(ctx context.Context, not *media.NewSegmentNotification) error { 35 - 36 - sid := livepeer.RandomTrailer(8) 37 - ctx = log.WithLogValues(ctx, "sid", sid) 38 - ctx, cancel := context.WithCancel(ctx) 39 - log.Log(ctx, "starting stream session") 40 - defer cancel() 41 - spseg, err := not.Segment.ToStreamplaceSegment() 42 - if err != nil { 43 - return fmt.Errorf("could not convert segment to streamplace segment: %w", err) 44 - } 45 - var allRenditions renditions.Renditions 46 - 47 - if ss.cli.LivepeerGatewayURL != "" { 48 - allRenditions, err = renditions.GenerateRenditions(spseg) 49 - } else { 50 - allRenditions = []renditions.Rendition{} 51 - } 52 - if err != nil { 53 - return err 54 - } 55 - if spseg.Duration == nil { 56 - return fmt.Errorf("segment duration is required to calculate bitrate") 57 - } 58 - dur := time.Duration(*spseg.Duration) 59 - byteLen := len(not.Data) 60 - bitrate := int(float64(byteLen) / dur.Seconds() * 8) 61 - sourceRendition := renditions.Rendition{ 62 - Name: "source", 63 - Bitrate: bitrate, 64 - Width: spseg.Video[0].Width, 65 - Height: spseg.Video[0].Height, 66 - } 67 - allRenditions = append([]renditions.Rendition{sourceRendition}, allRenditions...) 68 - ss.hls = media.NewM3U8(allRenditions) 69 - 70 - g, ctx := errgroup.WithContext(ctx) 71 - 72 - for _, r := range allRenditions { 73 - g.Go(func() error { 74 - for { 75 - if ctx.Err() != nil { 76 - return nil 77 - } 78 - err := ss.mm.ToHLS(ctx, spseg.Creator, r.Name, ss.hls) 79 - if ctx.Err() != nil { 80 - return nil 81 - } 82 - log.Warn(ctx, "hls failed, retrying in 5 seconds", "error", err) 83 - time.Sleep(time.Second * 5) 84 - } 85 - }) 86 - } 87 - 88 - for { 89 - select { 90 - case <-ss.segmentChan: 91 - // reset timer 92 - case <-ctx.Done(): 93 - return g.Wait() 94 - // case <-time.After(time.Minute * 1): 95 - case <-time.After(time.Second * 10): 96 - log.Log(ctx, "no new segments for 1 minute, shutting down") 97 - cancel() 98 - } 99 - } 100 - } 101 - 102 - func (ss *StreamSession) NewSegment(ctx context.Context, not *media.NewSegmentNotification) error { 103 - if ctx.Err() != nil { 104 - return nil 105 - } 106 - ss.segmentChan <- struct{}{} 107 - ctx = log.WithLogValues(ctx, "segID", not.Segment.ID) 108 - err := ss.mod.CreateSegment(not.Segment) 109 - if err != nil { 110 - return fmt.Errorf("could not add segment to database: %w", err) 111 - } 112 - spseg, err := not.Segment.ToStreamplaceSegment() 113 - if err != nil { 114 - return fmt.Errorf("could not convert segment to streamplace segment: %w", err) 115 - } 116 - 117 - ss.bus.Publish(spseg.Creator, spseg) 118 - 119 - go func() { 120 - err := ss.Thumbnail(ctx, spseg.Creator, not) 121 - if err != nil { 122 - log.Error(ctx, "could not create thumbnail", "error", err) 123 - } 124 - }() 125 - 126 - if ss.cli.LivepeerGatewayURL != "" { 127 - go func() { 128 - err := ss.Transcode(ctx, spseg, not.Data) 129 - if err != nil { 130 - log.Error(ctx, "could not transcode", "error", err) 131 - } 132 - }() 133 - } 134 - 135 - return nil 136 - } 137 - 138 - func (ss *StreamSession) Thumbnail(ctx context.Context, repoDID string, not *media.NewSegmentNotification) error { 139 - lock := thumbnail.GetThumbnailLock(not.Segment.RepoDID) 140 - locked := lock.TryLock() 141 - if !locked { 142 - // we're already generating a thumbnail for this user, skip 143 - return nil 144 - } 145 - defer lock.Unlock() 146 - oldThumb, err := ss.mod.LatestThumbnailForUser(not.Segment.RepoDID) 147 - if err != nil { 148 - return err 149 - } 150 - if oldThumb != nil && not.Segment.StartTime.Sub(oldThumb.Segment.StartTime) < time.Minute { 151 - // we have a thumbnail <60sec old, skip generating a new one 152 - return nil 153 - } 154 - r := bytes.NewReader(not.Data) 155 - aqt := aqtime.FromTime(not.Segment.StartTime) 156 - fd, err := ss.cli.SegmentFileCreate(not.Segment.RepoDID, aqt, "png") 157 - if err != nil { 158 - return err 159 - } 160 - defer fd.Close() 161 - err = ss.mm.Thumbnail(ctx, r, fd) 162 - if err != nil { 163 - return err 164 - } 165 - thumb := &model.Thumbnail{ 166 - Format: "png", 167 - SegmentID: not.Segment.ID, 168 - } 169 - err = ss.mod.CreateThumbnail(thumb) 170 - if err != nil { 171 - return err 172 - } 173 - return nil 174 - } 175 - 176 - func (ss *StreamSession) Transcode(ctx context.Context, spseg *streamplace.Segment, data []byte) error { 177 - rs, err := renditions.GenerateRenditions(spseg) 178 - if ss.lp == nil { 179 - var err error 180 - ss.lp, err = livepeer.NewLivepeerSession(ctx, spseg.Creator, ss.cli.LivepeerGatewayURL) 181 - if err != nil { 182 - return err 183 - } 184 - 185 - } 186 - segs, err := ss.lp.PostSegmentToGateway(ctx, data) 187 - if err != nil { 188 - return err 189 - } 190 - if len(rs) != len(segs) { 191 - return fmt.Errorf("expected %d renditions, got %d", len(rs), len(segs)) 192 - } 193 - aqt, err := aqtime.FromString(spseg.StartTime) 194 - if err != nil { 195 - return err 196 - } 197 - for i, seg := range segs { 198 - log.Debug(ctx, "publishing segment", "rendition", rs[i]) 199 - fd, err := ss.cli.SegmentFileCreate(spseg.Creator, aqt, fmt.Sprintf("%s.mp4", rs[i].Name)) 200 - if err != nil { 201 - return err 202 - } 203 - defer fd.Close() 204 - fd.Write(seg) 205 - // go ss.TryAddToHLS(ctx, spseg, rs[i].Name, seg) 206 - go ss.mm.PublishSegment(ctx, spseg.Creator, rs[i].Name, &segchanman.Seg{ 207 - Filepath: fd.Name(), 208 - Data: seg, 209 - }) 210 - } 211 - return nil 212 - } 213 - 214 - // func (ss *StreamSession) TryAddToHLS(ctx context.Context, spseg *streamplace.Segment, rendition string, data []byte) { 215 - // ctx = log.WithLogValues(ctx, "rendition", rendition) 216 - // err := ss.AddToHLS(ctx, spseg, rendition, data) 217 - // if err != nil { 218 - // log.Error(ctx, "could not add to hls", "error", err) 219 - // } 220 - // } 221 - 222 - // func (ss *StreamSession) AddToHLS(ctx context.Context, spseg *streamplace.Segment, rendition string, data []byte) error { 223 - // buf := bytes.Buffer{} 224 - // dur, err := media.MP4ToMPEGTS(ctx, bytes.NewReader(data), &buf) 225 - // if err != nil { 226 - // return err 227 - // } 228 - // newSeg := &streamplace.Segment{ 229 - // LexiconTypeID: "place.stream.segment", 230 - // Id: spseg.Id, 231 - // Creator: spseg.Creator, 232 - // StartTime: spseg.StartTime, 233 - // Duration: &dur, 234 - // Audio: spseg.Audio, 235 - // Video: spseg.Video, 236 - // SigningKey: spseg.SigningKey, 237 - // } 238 - // log.Debug(ctx, "transmuxed to mpegts, adding to hls", "rendition", rendition, "size", buf.Len()) 239 - // ss.hls.NewSegment(newSeg, rendition, buf.Bytes()) 240 - // return nil 241 - // }
-1
pkg/gen/gen.go
··· 25 25 streamplace.Segment{}, 26 26 streamplace.Segment_Audio{}, 27 27 streamplace.Segment_Video{}, 28 - streamplace.Segment_Framerate{}, 29 28 streamplace.ChatMessage{}, 30 29 streamplace.RichtextFacet{}, 31 30 streamplace.ChatProfile{},
-96
pkg/livepeer/livepeer.go
··· 1 - package livepeer 2 - 3 - import ( 4 - "bytes" 5 - "context" 6 - "fmt" 7 - "io" 8 - "math/rand" 9 - "mime" 10 - "mime/multipart" 11 - "net/http" 12 - "strings" 13 - 14 - "golang.org/x/net/context/ctxhttp" 15 - "stream.place/streamplace/pkg/aqhttp" 16 - "stream.place/streamplace/pkg/log" 17 - ) 18 - 19 - type LivepeerSession struct { 20 - SessionID string 21 - Count int 22 - GatewayURL string 23 - } 24 - 25 - // borrowed from catalyst-api 26 - func RandomTrailer(length int) string { 27 - const charset = "abcdefghijklmnopqrstuvwxyz0123456789" 28 - 29 - res := make([]byte, length) 30 - for i := 0; i < length; i++ { 31 - res[i] = charset[rand.Intn(len(charset))] 32 - } 33 - return string(res) 34 - } 35 - 36 - func NewLivepeerSession(ctx context.Context, did string, gatewayURL string) (*LivepeerSession, error) { 37 - sessionID := RandomTrailer(8) 38 - return &LivepeerSession{ 39 - SessionID: fmt.Sprintf("%s-%s", did, sessionID), 40 - Count: 0, 41 - GatewayURL: gatewayURL, 42 - }, nil 43 - } 44 - 45 - func (ls *LivepeerSession) PostSegmentToGateway(ctx context.Context, buf []byte) ([][]byte, error) { 46 - url := fmt.Sprintf("%s/live/%s/%d.mp4", ls.GatewayURL, ls.SessionID, ls.Count) 47 - ls.Count++ 48 - 49 - req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(buf)) 50 - if err != nil { 51 - return nil, fmt.Errorf("failed to create request: %w", err) 52 - } 53 - req.Header.Set("Accept", "multipart/mixed") 54 - 55 - resp, err := ctxhttp.Do(ctx, &aqhttp.Client, req) 56 - if err != nil { 57 - return nil, fmt.Errorf("failed to send segment to gateway: %w", err) 58 - } 59 - ctx, cancel := context.WithCancel(ctx) 60 - defer cancel() 61 - go func() { 62 - <-ctx.Done() 63 - resp.Body.Close() 64 - }() 65 - 66 - if resp.StatusCode != http.StatusOK { 67 - return nil, fmt.Errorf("gateway returned non-OK status: %d", resp.StatusCode) 68 - } 69 - 70 - var out [][]byte 71 - 72 - mediaType, params, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) 73 - if err != nil { 74 - return nil, fmt.Errorf("failed to parse media type: %w", err) 75 - } 76 - if strings.HasPrefix(mediaType, "multipart/") { 77 - mr := multipart.NewReader(resp.Body, params["boundary"]) 78 - for { 79 - p, err := mr.NextPart() 80 - if err == io.EOF { 81 - break 82 - } 83 - if err != nil { 84 - return nil, fmt.Errorf("failed to get next part: %w", err) 85 - } 86 - bs, err := io.ReadAll(p) 87 - if err != nil { 88 - return nil, fmt.Errorf("failed to read part: %w", err) 89 - } 90 - log.Debug(ctx, "got part back from livepeer gateway", "length", len(bs), "name", p.FileName()) 91 - out = append(out, bs) 92 - } 93 - } 94 - 95 - return out, nil 96 - }
+1 -1
pkg/media/bus_handler.go
··· 26 26 } 27 27 switch msg.Type() { 28 28 case gst.MessageEOS: // When end-of-stream is received flush the pipeline and stop the main loop 29 - log.Debug(ctx, "got gst.MessageEOS, exiting") 29 + log.Log(ctx, "got gst.MessageEOS, exiting") 30 30 return 31 31 case gst.MessageError: // Error messages are always fatal 32 32 err := msg.ParseError()
+27 -17
pkg/media/concat.go
··· 1 1 package media 2 2 3 3 import ( 4 - "bytes" 5 4 "context" 6 5 "errors" 7 6 "fmt" 8 7 "io" 8 + "os" 9 9 "strings" 10 10 "sync" 11 11 12 12 "github.com/go-gst/go-gst/gst" 13 13 "github.com/go-gst/go-gst/gst/app" 14 14 "stream.place/streamplace/pkg/log" 15 - "stream.place/streamplace/pkg/media/segchanman" 16 15 ) 17 16 18 17 type ConcatStreamer interface { 19 - SubscribeSegment(ctx context.Context, user string, rendition string) <-chan *segchanman.Seg 20 - UnsubscribeSegment(ctx context.Context, user string, rendition string, ch <-chan *segchanman.Seg) 18 + SubscribeSegment(ctx context.Context, user string) <-chan string 19 + UnsubscribeSegment(ctx context.Context, user string, ch <-chan string) 21 20 } 22 21 23 22 // This function remains in scope for the duration of a single users' playback 24 - func ConcatStream(ctx context.Context, pipeline *gst.Pipeline, user string, rendition string, streamer ConcatStreamer) (*gst.Element, <-chan struct{}, error) { 23 + func ConcatStream(ctx context.Context, pipeline *gst.Pipeline, user string, streamer ConcatStreamer) (*gst.Element, <-chan struct{}, error) { 25 24 ctx = log.WithLogValues(ctx, "func", "ConcatStream") 26 25 ctx, cancel := context.WithCancel(ctx) 27 26 ··· 35 34 err = pipeline.Add(inputQueue) 36 35 if err != nil { 37 36 return nil, nil, fmt.Errorf("failed to add input multiqueue to pipeline: %w", err) 37 + } 38 + for _, tmpl := range inputQueue.GetPadTemplates() { 39 + log.Warn(ctx, "pad template", "name", tmpl.GetName(), "direction", tmpl.Direction()) 38 40 } 39 41 inputQueuePadVideoSink := inputQueue.GetRequestPad("sink_%u") 40 42 if inputQueuePadVideoSink == nil { ··· 123 125 124 126 // this goroutine will read all the files from the segment queue and buffer 125 127 // them in a pipe so that we don't miss any in between iterations of the output 126 - allFiles := make(chan []byte, 1024) 128 + allFiles := make(chan string, 1024) 127 129 go func() { 128 130 for { 129 - ch := streamer.SubscribeSegment(ctx, user, rendition) 131 + ch := streamer.SubscribeSegment(ctx, user) 130 132 select { 131 133 case <-ctx.Done(): 132 - log.Debug(ctx, "exiting segment reader") 133 - streamer.UnsubscribeSegment(ctx, user, rendition, ch) 134 + log.Warn(ctx, "exiting segment reader") 135 + streamer.UnsubscribeSegment(ctx, user, ch) 134 136 return 135 137 case file := <-ch: 136 - log.Debug(ctx, "got segment", "file", file.Filepath) 137 - allFiles <- file.Data 138 - if len(file.Data) == 0 { 138 + log.Debug(ctx, "got segment", "file", file) 139 + allFiles <- file 140 + if file == "" { 139 141 log.Warn(ctx, "no more segments") 140 142 return 141 143 } ··· 154 156 pr.Close() 155 157 pw.Close() 156 158 return 157 - case bs := <-allFiles: 158 - if len(bs) == 0 { 159 + case fullpath := <-allFiles: 160 + if fullpath == "" { 159 161 log.Warn(ctx, "no more segments") 160 162 cancel() 161 163 return 162 164 } 163 - _, err = io.Copy(pw, bytes.NewReader(bs)) 165 + f, err := os.Open(fullpath) 166 + log.Debug(ctx, "opening segment file", "file", fullpath) 164 167 if err != nil { 165 - log.Error(ctx, "failed to copy segment file", "error", err) 168 + log.Debug(ctx, "failed to open segment file", "error", err, "file", fullpath) 169 + cancel() 170 + return 171 + } 172 + defer f.Close() 173 + _, err = io.Copy(pw, f) 174 + if err != nil { 175 + log.Error(ctx, "failed to copy segment file", "error", err, "file", fullpath) 166 176 cancel() 167 177 return 168 178 } ··· 294 304 done() 295 305 return 296 306 } else { 297 - log.Debug(ctx, "failed to read data, ending stream", "error", err) 307 + log.Error(ctx, "failed to read data", "error", err) 298 308 cancel() 299 309 return 300 310 }
-95
pkg/media/ffmpeg_concat.go
··· 1 - package media 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "io" 7 - 8 - "github.com/livepeer/lpms/ffmpeg" 9 - "golang.org/x/sync/errgroup" 10 - ) 11 - 12 - func (mm *MediaManager) SegmentToMKV(ctx context.Context, user string, rendition string, w io.Writer) error { 13 - muxer := ffmpeg.ComponentOptions{ 14 - Name: "matroska", 15 - } 16 - return mm.SegmentToStream(ctx, user, rendition, muxer, w) 17 - } 18 - 19 - func (mm *MediaManager) SegmentToMKVPlusOpus(ctx context.Context, user string, rendition string, w io.Writer) error { 20 - muxer := ffmpeg.ComponentOptions{ 21 - Name: "matroska", 22 - } 23 - pr, pw := io.Pipe() 24 - g, ctx := errgroup.WithContext(ctx) 25 - g.Go(func() error { 26 - return mm.SegmentToStream(ctx, user, rendition, muxer, pw) 27 - }) 28 - g.Go(func() error { 29 - return AddOpusToMKV(ctx, pr, w) 30 - }) 31 - return g.Wait() 32 - } 33 - 34 - func (mm *MediaManager) SegmentToMP4(ctx context.Context, user string, rendition string, w io.Writer) error { 35 - muxer := ffmpeg.ComponentOptions{ 36 - Name: "mp4", 37 - Opts: map[string]string{ 38 - "movflags": "frag_keyframe+empty_moov", 39 - }, 40 - } 41 - return mm.SegmentToStream(ctx, user, rendition, muxer, w) 42 - } 43 - 44 - func (mm *MediaManager) SegmentToStream(ctx context.Context, user string, rendition string, muxer ffmpeg.ComponentOptions, w io.Writer) error { 45 - tc := ffmpeg.NewTranscoder() 46 - defer tc.StopTranscoder() 47 - ourl, or, odone, err := mm.HTTPPipe() 48 - if err != nil { 49 - return err 50 - } 51 - defer odone() 52 - iname := fmt.Sprintf("%s/playback/%s/%s/concat", mm.cli.OwnInternalURL(), user, rendition) 53 - in := &ffmpeg.TranscodeOptionsIn{ 54 - Fname: iname, 55 - Transmuxing: true, 56 - Profile: ffmpeg.VideoProfile{}, 57 - Loop: -1, 58 - Demuxer: ffmpeg.ComponentOptions{ 59 - Name: "concat", 60 - Opts: map[string]string{ 61 - "safe": "0", 62 - "protocol_whitelist": "file,http,https,tcp,tls", 63 - }, 64 - }, 65 - } 66 - out := []ffmpeg.TranscodeOptions{ 67 - { 68 - Oname: ourl, 69 - VideoEncoder: ffmpeg.ComponentOptions{ 70 - Name: "copy", 71 - }, 72 - AudioEncoder: ffmpeg.ComponentOptions{ 73 - Name: "copy", 74 - }, 75 - Profile: ffmpeg.VideoProfile{Format: ffmpeg.FormatNone}, 76 - Muxer: muxer, 77 - }, 78 - } 79 - g, _ := errgroup.WithContext(ctx) 80 - g.Go(func() error { 81 - <-ctx.Done() 82 - or.Close() 83 - return nil 84 - }) 85 - g.Go(func() error { 86 - _, err := tc.Transcode(in, out) 87 - tc.StopTranscoder() 88 - return err 89 - }) 90 - g.Go(func() error { 91 - _, err := io.Copy(w, or) 92 - return err 93 - }) 94 - return g.Wait() 95 - }
+638
pkg/media/gstreamer.go
··· 15 15 "github.com/go-gst/go-glib/glib" 16 16 "github.com/go-gst/go-gst/gst" 17 17 "github.com/go-gst/go-gst/gst/app" 18 + "github.com/google/uuid" 18 19 "github.com/skip2/go-qrcode" 19 20 "golang.org/x/sync/errgroup" 20 21 "stream.place/streamplace/pkg/aqtime" 21 22 "stream.place/streamplace/pkg/log" 23 + "stream.place/streamplace/pkg/model" 22 24 "stream.place/streamplace/test" 23 25 ) 24 26 ··· 230 232 return nil 231 233 } 232 234 235 + // #EXTM3U 236 + // #EXT-X-VERSION:3 237 + // #EXT-X-MEDIA-SEQUENCE:281 238 + // #EXT-X-TARGETDURATION:1 239 + 240 + // #EXTINF:1, 241 + // segment00281.ts 242 + // #EXTINF:1.0049999952316284, 243 + // segment00282.ts 244 + // #EXTINF:1, 245 + // segment00283.ts 246 + // #EXTINF:1.0010000467300415, 247 + // segment00284.ts 248 + // #EXTINF:1, 249 + // segment00285.ts 250 + // #EXT-X-ENDLIST 251 + 252 + func (mm *MediaManager) ToHLS(ctx context.Context, input io.Reader, m3u8 *M3U8) error { 253 + ctx = log.WithLogValues(ctx, "GStreamerFunc", "ToHLS") 254 + 255 + splitmuxsink, err := gst.NewElementWithProperties("splitmuxsink", map[string]any{ 256 + "name": "mux", 257 + "async-finalize": true, 258 + "sink-factory": "appsink", 259 + "muxer-factory": "mpegtsmux", 260 + "max-size-bytes": 1, 261 + }) 262 + if err != nil { 263 + return err 264 + } 265 + 266 + p := splitmuxsink.GetRequestPad("video") 267 + if p == nil { 268 + return fmt.Errorf("failed to get video pad") 269 + } 270 + p = splitmuxsink.GetRequestPad("audio_%u") 271 + if p == nil { 272 + return fmt.Errorf("failed to get audio pad") 273 + } 274 + 275 + pipelineSlice := []string{ 276 + "appsrc name=appsrc ! matroskademux name=demux", 277 + "demux.video_0 ! queue ! h264parse name=videoparse", 278 + "demux.audio_0 ! queue ! opusdec use-inband-fec=true ! audioresample ! fdkaacenc name=audioenc", 279 + } 280 + 281 + pipeline, err := gst.NewPipelineFromString(strings.Join(pipelineSlice, "\n")) 282 + if err != nil { 283 + return fmt.Errorf("error creating ToHLS pipeline: %w", err) 284 + } 285 + 286 + err = pipeline.Add(splitmuxsink) 287 + if err != nil { 288 + return fmt.Errorf("error adding splitmuxsink to ToHLS pipeline: %w", err) 289 + } 290 + 291 + videoparse, err := pipeline.GetElementByName("videoparse") 292 + if err != nil { 293 + return fmt.Errorf("error getting videoparse from ToHLS pipeline: %w", err) 294 + } 295 + err = videoparse.Link(splitmuxsink) 296 + if err != nil { 297 + return fmt.Errorf("error linking videoparse to splitmuxsink: %w", err) 298 + } 299 + 300 + audioenc, err := pipeline.GetElementByName("audioenc") 301 + if err != nil { 302 + return fmt.Errorf("error getting audioenc from ToHLS pipeline: %w", err) 303 + } 304 + err = audioenc.Link(splitmuxsink) 305 + if err != nil { 306 + return fmt.Errorf("error linking audioenc to splitmuxsink: %w", err) 307 + } 308 + 309 + splitmuxsink.Connect("sink-added", func(split, sinkEle *gst.Element) { 310 + vf, err := m3u8.GetNextSegment(ctx) 311 + if err != nil { 312 + panic(err) 313 + } 314 + appsink := app.SinkFromElement(sinkEle) 315 + appsink.SetCallbacks(&app.SinkCallbacks{ 316 + NewSampleFunc: WriterNewSample(ctx, vf.Buf), 317 + EOSFunc: func(sink *app.Sink) { 318 + m3u8.CloseSegment(ctx, vf) 319 + }, 320 + }) 321 + }) 322 + 323 + appsrc, err := pipeline.GetElementByName("appsrc") 324 + if err != nil { 325 + return err 326 + } 327 + 328 + src := app.SrcFromElement(appsrc) 329 + src.SetCallbacks(&app.SourceCallbacks{ 330 + NeedDataFunc: ReaderNeedData(ctx, input), 331 + }) 332 + 333 + onPadAdded := func(element *gst.Element, pad *gst.Pad) { 334 + caps := pad.GetCurrentCaps() 335 + if caps == nil { 336 + fmt.Println("Unable to get pad caps") 337 + return 338 + } 339 + 340 + fmt.Printf("New pad added: %s\n", pad.GetName()) 341 + fmt.Printf("Caps: %s\n", caps.String()) 342 + 343 + structure := caps.GetStructureAt(0) 344 + if structure == nil { 345 + fmt.Println("Unable to get structure from caps") 346 + return 347 + } 348 + 349 + name := structure.Name() 350 + fmt.Printf("Structure Name: %s\n", name) 351 + 352 + if name[:5] == "video" { 353 + // Get some common video properties 354 + widthVal, _ := structure.GetValue("width") 355 + heightVal, _ := structure.GetValue("height") 356 + 357 + width, ok := widthVal.(int) 358 + if ok { 359 + m3u8.Width = uint64(width) 360 + } 361 + height, ok := heightVal.(int) 362 + if ok { 363 + m3u8.Height = uint64(height) 364 + } 365 + // framerate, ok := framerateVal.(string) 366 + // if ok { 367 + // fmt.Printf(" Framerate: %s\n", framerate) 368 + // } 369 + // pixelAspectRatio, ok := pixelAspectRatioVal.(string) 370 + // if ok { 371 + // fmt.Printf(" Pixel Aspect Ratio: %s\n", pixelAspectRatio) 372 + // } 373 + // if codecVal != nil { 374 + // fmt.Printf(" Has codec data: true\n") 375 + // } 376 + } 377 + 378 + // if name[:5] == "audio" { 379 + // // Get some common audio properties 380 + // rateVal, _ := structure.GetValue("rate") 381 + // channelsVal, _ := structure.GetValue("channels") 382 + // formatVal, err := structure.GetValue("format") 383 + // mpegversion, _ := structure.GetValue("mpegversion") 384 + // log.Log(ctx, "format error", "error", err, "mpegversion", mpegversion) 385 + 386 + // fmt.Printf(" Structure: %s\n", structure.String()) 387 + // rate, ok := rateVal.(int) 388 + // if ok { 389 + // fmt.Printf(" Rate: %d\n", rate) 390 + // } 391 + // channels, ok := channelsVal.(int) 392 + // if ok { 393 + // fmt.Printf(" Channels: %d\n", channels) 394 + // } 395 + // format, ok := formatVal.(int) 396 + // if ok { 397 + // fmt.Printf(" Format: %d\n", format) 398 + // } 399 + 400 + // } 401 + } 402 + 403 + demux, err := pipeline.GetElementByName("demux") 404 + if err != nil { 405 + return err 406 + } 407 + demux.Connect("pad-added", onPadAdded) 408 + 409 + ctx, cancel := context.WithCancel(ctx) 410 + defer cancel() 411 + go func() { 412 + HandleBusMessagesCustom(ctx, pipeline, func(msg *gst.Message) { 413 + switch msg.Type() { 414 + case gst.MessageElement: 415 + structure := msg.GetStructure() 416 + name := structure.Name() 417 + if name == "splitmuxsink-fragment-opened" { 418 + runningTime, err := structure.GetValue("running-time") 419 + if err != nil { 420 + log.Warn(ctx, "splitmuxsink-fragment-opened error", "error", err) 421 + cancel() 422 + } 423 + runningTimeInt, ok := runningTime.(uint64) 424 + if !ok { 425 + log.Warn(ctx, "splitmuxsink-fragment-opened not a uint64") 426 + cancel() 427 + } 428 + m3u8.FragmentOpened(ctx, runningTimeInt) 429 + } 430 + if name == "splitmuxsink-fragment-closed" { 431 + runningTime, err := structure.GetValue("running-time") 432 + if err != nil { 433 + log.Warn(ctx, "splitmuxsink-fragment-closed error", "error", err) 434 + cancel() 435 + } 436 + runningTimeInt, ok := runningTime.(uint64) 437 + if !ok { 438 + log.Warn(ctx, "splitmuxsink-fragment-closed not a uint64") 439 + cancel() 440 + } 441 + m3u8.FragmentClosed(ctx, runningTimeInt) 442 + } 443 + } 444 + }) 445 + cancel() 446 + }() 447 + 448 + // Start the pipeline 449 + pipeline.SetState(gst.StatePlaying) 450 + 451 + <-ctx.Done() 452 + 453 + pipeline.BlockSetState(gst.StateNull) 454 + 455 + return nil 456 + } 457 + 233 458 func (mm *MediaManager) IngestStream(ctx context.Context, input io.Reader, ms MediaSigner) error { 234 459 ctx, cancel := context.WithCancel(ctx) 235 460 defer cancel() ··· 418 643 419 644 return g.Wait() 420 645 } 646 + 647 + // element that takes the input stream, muxes to mp4, and signs the result 648 + func (mm *MediaManager) SegmentAndSignElem(ctx context.Context, ms MediaSigner) (*gst.Element, error) { 649 + // elem, err := gst.NewElement("splitmuxsink name=splitter async-finalize=true sink-factory=appsink muxer-factory=matroskamux max-size-bytes=1") 650 + elem, err := gst.NewElementWithProperties("splitmuxsink", map[string]any{ 651 + "name": "signer", 652 + "async-finalize": true, 653 + "sink-factory": "appsink", 654 + "muxer-factory": "mp4mux", 655 + "max-size-bytes": 1, 656 + }) 657 + if err != nil { 658 + return nil, err 659 + } 660 + 661 + p := elem.GetRequestPad("video") 662 + if p == nil { 663 + return nil, fmt.Errorf("failed to get video pad") 664 + } 665 + p = elem.GetRequestPad("audio_%u") 666 + if p == nil { 667 + return nil, fmt.Errorf("failed to get audio pad") 668 + } 669 + 670 + resetTimer := make(chan struct{}) 671 + 672 + go func() { 673 + for { 674 + select { 675 + case <-ctx.Done(): 676 + return 677 + case <-resetTimer: 678 + continue 679 + case <-time.After(time.Second * 10): 680 + log.Warn(ctx, "no new segment for 10 seconds") 681 + elem.ErrorMessage(gst.DomainCore, gst.CoreErrorFailed, "No new segment for 10 seconds", "No new segment for 10 seconds (debug)") 682 + return 683 + } 684 + } 685 + }() 686 + 687 + elem.Connect("sink-added", func(split, sinkEle *gst.Element) { 688 + buf := &bytes.Buffer{} 689 + appsink := app.SinkFromElement(sinkEle) 690 + if appsink == nil { 691 + panic("appsink should not be nil") 692 + } 693 + appsink.SetCallbacks(&app.SinkCallbacks{ 694 + NewSampleFunc: WriterNewSample(ctx, buf), 695 + EOSFunc: func(sink *app.Sink) { 696 + resetTimer <- struct{}{} 697 + bs, err := ms.SignMP4(ctx, bytes.NewReader(buf.Bytes()), time.Now().UnixMilli()) 698 + if err != nil { 699 + log.Error(ctx, "error signing segment", "error", err) 700 + return 701 + } 702 + err = mm.ValidateMP4(ctx, bytes.NewReader(bs)) 703 + if err != nil { 704 + log.Error(ctx, "error validating segment", "error", err) 705 + return 706 + } 707 + }, 708 + }) 709 + }) 710 + 711 + return elem, nil 712 + } 713 + 714 + func (mm *MediaManager) Thumbnail(ctx context.Context, r io.Reader, w io.Writer) error { 715 + ctx = log.WithLogValues(ctx, "function", "Thumbnail") 716 + ctx, cancel := context.WithCancel(ctx) 717 + defer cancel() 718 + 719 + pipelineSlice := []string{ 720 + "appsrc name=appsrc ! qtdemux ! decodebin ! videoconvert ! videoscale ! video/x-raw,width=[1,720],height=[1,720],pixel-aspect-ratio=1/1 ! pngenc snapshot=true ! appsink name=appsink", 721 + } 722 + 723 + pipeline, err := gst.NewPipelineFromString(strings.Join(pipelineSlice, "\n")) 724 + if err != nil { 725 + return fmt.Errorf("error creating Thumbnail pipeline: %w", err) 726 + } 727 + appsrc, err := pipeline.GetElementByName("appsrc") 728 + if err != nil { 729 + return err 730 + } 731 + 732 + src := app.SrcFromElement(appsrc) 733 + src.SetCallbacks(&app.SourceCallbacks{ 734 + NeedDataFunc: ReaderNeedData(ctx, r), 735 + }) 736 + 737 + appsink, err := pipeline.GetElementByName("appsink") 738 + if err != nil { 739 + return err 740 + } 741 + 742 + go func() { 743 + HandleBusMessages(ctx, pipeline) 744 + cancel() 745 + }() 746 + 747 + sink := app.SinkFromElement(appsink) 748 + sink.SetCallbacks(&app.SinkCallbacks{ 749 + NewSampleFunc: WriterNewSample(ctx, w), 750 + EOSFunc: func(sink *app.Sink) { 751 + cancel() 752 + }, 753 + }) 754 + 755 + pipeline.SetState(gst.StatePlaying) 756 + 757 + <-ctx.Done() 758 + 759 + pipeline.BlockSetState(gst.StateNull) 760 + 761 + return nil 762 + } 763 + 764 + func (mm *MediaManager) MP4Playback(ctx context.Context, user string, w io.Writer) error { 765 + uu, err := uuid.NewV7() 766 + if err != nil { 767 + return err 768 + } 769 + ctx = log.WithLogValues(ctx, "playbackID", uu.String()) 770 + ctx, cancel := context.WithCancel(ctx) 771 + 772 + ctx = log.WithLogValues(ctx, "mediafunc", "MP4Playback") 773 + 774 + pipelineSlice := []string{ 775 + "mp4mux name=muxer fragment-mode=first-moov-then-finalise fragment-duration=1000 streamable=true ! appsink name=mp4sink", 776 + "h264parse name=videoparse ! muxer.", 777 + "opusparse name=audioparse ! muxer.", 778 + } 779 + 780 + pipeline, err := gst.NewPipelineFromString(strings.Join(pipelineSlice, "\n")) 781 + if err != nil { 782 + return fmt.Errorf("failed to create GStreamer pipeline: %w", err) 783 + } 784 + 785 + go func() { 786 + HandleBusMessages(ctx, pipeline) 787 + cancel() 788 + }() 789 + 790 + outputQueue, done, err := ConcatStream(ctx, pipeline, user, mm) 791 + if err != nil { 792 + return fmt.Errorf("failed to get output queue: %w", err) 793 + } 794 + go func() { 795 + select { 796 + case <-ctx.Done(): 797 + return 798 + case <-done: 799 + cancel() 800 + } 801 + }() 802 + 803 + videoParse, err := pipeline.GetElementByName("videoparse") 804 + if err != nil { 805 + return fmt.Errorf("failed to get video sink element from pipeline: %w", err) 806 + } 807 + err = outputQueue.Link(videoParse) 808 + if err != nil { 809 + return fmt.Errorf("failed to link output queue to video parse: %w", err) 810 + } 811 + 812 + audioParse, err := pipeline.GetElementByName("audioparse") 813 + if err != nil { 814 + return fmt.Errorf("failed to get audio parse element from pipeline: %w", err) 815 + } 816 + err = outputQueue.Link(audioParse) 817 + if err != nil { 818 + return fmt.Errorf("failed to link output queue to audio parse: %w", err) 819 + } 820 + 821 + go func() { 822 + ticker := time.NewTicker(time.Second * 1) 823 + for { 824 + select { 825 + case <-ctx.Done(): 826 + return 827 + case <-ticker.C: 828 + state := pipeline.GetCurrentState() 829 + log.Debug(ctx, "pipeline state", "state", state) 830 + } 831 + } 832 + }() 833 + 834 + mp4sinkele, err := pipeline.GetElementByName("mp4sink") 835 + if err != nil { 836 + return fmt.Errorf("failed to get video sink element from pipeline: %w", err) 837 + } 838 + mp4sink := app.SinkFromElement(mp4sinkele) 839 + mp4sink.SetCallbacks(&app.SinkCallbacks{ 840 + NewSampleFunc: WriterNewSample(ctx, w), 841 + EOSFunc: func(sink *app.Sink) { 842 + log.Warn(ctx, "mp4sink EOSFunc") 843 + cancel() 844 + }, 845 + }) 846 + 847 + pipeline.SetState(gst.StatePlaying) 848 + 849 + <-ctx.Done() 850 + 851 + pipeline.BlockSetState(gst.StateNull) 852 + 853 + return nil 854 + } 855 + 856 + func (mm *MediaManager) MKVPlayback(ctx context.Context, user string, w io.Writer) error { 857 + uu, err := uuid.NewV7() 858 + if err != nil { 859 + return err 860 + } 861 + ctx = log.WithLogValues(ctx, "playbackID", uu.String()) 862 + ctx, cancel := context.WithCancel(ctx) 863 + 864 + ctx = log.WithLogValues(ctx, "mediafunc", "MKVPlayback") 865 + 866 + pipelineSlice := []string{ 867 + "matroskamux name=muxer streamable=true ! appsink name=mkvsink", 868 + "h264parse name=videoparse ! muxer.", 869 + "opusparse name=audioparse ! muxer.", 870 + } 871 + 872 + pipeline, err := gst.NewPipelineFromString(strings.Join(pipelineSlice, "\n")) 873 + if err != nil { 874 + return fmt.Errorf("failed to create GStreamer pipeline: %w", err) 875 + } 876 + 877 + go func() { 878 + HandleBusMessages(ctx, pipeline) 879 + cancel() 880 + }() 881 + 882 + outputQueue, done, err := ConcatStream(ctx, pipeline, user, mm) 883 + if err != nil { 884 + return fmt.Errorf("failed to get output queue: %w", err) 885 + } 886 + go func() { 887 + select { 888 + case <-ctx.Done(): 889 + return 890 + case <-done: 891 + cancel() 892 + } 893 + }() 894 + 895 + videoParse, err := pipeline.GetElementByName("videoparse") 896 + if err != nil { 897 + return fmt.Errorf("failed to get video sink element from pipeline: %w", err) 898 + } 899 + err = outputQueue.Link(videoParse) 900 + if err != nil { 901 + return fmt.Errorf("failed to link output queue to video parse: %w", err) 902 + } 903 + 904 + audioParse, err := pipeline.GetElementByName("audioparse") 905 + if err != nil { 906 + return fmt.Errorf("failed to get audio parse element from pipeline: %w", err) 907 + } 908 + err = outputQueue.Link(audioParse) 909 + if err != nil { 910 + return fmt.Errorf("failed to link output queue to audio parse: %w", err) 911 + } 912 + 913 + go func() { 914 + ticker := time.NewTicker(time.Second * 1) 915 + for { 916 + select { 917 + case <-ctx.Done(): 918 + return 919 + case <-ticker.C: 920 + state := pipeline.GetCurrentState() 921 + log.Debug(ctx, "pipeline state", "state", state) 922 + } 923 + } 924 + }() 925 + 926 + mkvsinkele, err := pipeline.GetElementByName("mkvsink") 927 + if err != nil { 928 + return fmt.Errorf("failed to get video sink element from pipeline: %w", err) 929 + } 930 + mkvsink := app.SinkFromElement(mkvsinkele) 931 + mkvsink.SetCallbacks(&app.SinkCallbacks{ 932 + NewSampleFunc: WriterNewSample(ctx, w), 933 + EOSFunc: func(sink *app.Sink) { 934 + log.Warn(ctx, "mp4sink EOSFunc") 935 + cancel() 936 + }, 937 + }) 938 + 939 + pipeline.SetState(gst.StatePlaying) 940 + 941 + <-ctx.Done() 942 + 943 + pipeline.BlockSetState(gst.StateNull) 944 + 945 + return nil 946 + } 947 + 948 + func (mm *MediaManager) ParseSegmentMediaData(ctx context.Context, mp4bs []byte) (*model.SegmentMediaData, error) { 949 + ctx = log.WithLogValues(ctx, "GStreamerFunc", "ParseSegmentMediaData") 950 + ctx, cancel := context.WithCancel(ctx) 951 + defer cancel() 952 + pipelineSlice := []string{ 953 + "appsrc name=appsrc ! qtdemux name=demux ! fakesink", 954 + } 955 + 956 + pipeline, err := gst.NewPipelineFromString(strings.Join(pipelineSlice, "\n")) 957 + if err != nil { 958 + return nil, fmt.Errorf("error creating SegmentMetadata pipeline: %w", err) 959 + } 960 + 961 + var videoMetadata *model.SegmentMediadataVideo 962 + var audioMetadata *model.SegmentMediadataAudio 963 + 964 + appsrc, err := pipeline.GetElementByName("appsrc") 965 + if err != nil { 966 + return nil, fmt.Errorf("error creating SegmentMetadata pipeline: %w", err) 967 + } 968 + 969 + src := app.SrcFromElement(appsrc) 970 + src.SetCallbacks(&app.SourceCallbacks{ 971 + NeedDataFunc: ReaderNeedData(ctx, bytes.NewReader(mp4bs)), 972 + }) 973 + 974 + onPadAdded := func(element *gst.Element, pad *gst.Pad) { 975 + caps := pad.GetCurrentCaps() 976 + if caps == nil { 977 + log.Warn(ctx, "Unable to get pad caps") 978 + cancel() 979 + return 980 + } 981 + 982 + structure := caps.GetStructureAt(0) 983 + if structure == nil { 984 + log.Warn(ctx, "Unable to get structure from caps") 985 + cancel() 986 + return 987 + } 988 + 989 + name := structure.Name() 990 + log.Debug(ctx, "Structure Name", "name", name) 991 + 992 + if name[:5] == "video" { 993 + videoMetadata = &model.SegmentMediadataVideo{} 994 + // Get some common video properties 995 + widthVal, _ := structure.GetValue("width") 996 + heightVal, _ := structure.GetValue("height") 997 + 998 + width, ok := widthVal.(int) 999 + if ok { 1000 + videoMetadata.Width = width 1001 + } 1002 + height, ok := heightVal.(int) 1003 + if ok { 1004 + videoMetadata.Height = height 1005 + } 1006 + framerateVal, _ := structure.GetValue("framerate") 1007 + framerateStr := fmt.Sprintf("%v", framerateVal) 1008 + if framerateStr != "" { 1009 + videoMetadata.Framerate = framerateStr 1010 + } 1011 + } 1012 + 1013 + if name[:5] == "audio" { 1014 + audioMetadata = &model.SegmentMediadataAudio{} 1015 + // Get some common audio properties 1016 + rateVal, _ := structure.GetValue("rate") 1017 + channelsVal, _ := structure.GetValue("channels") 1018 + 1019 + rate, ok := rateVal.(int) 1020 + if ok { 1021 + audioMetadata.Rate = rate 1022 + } 1023 + channels, ok := channelsVal.(int) 1024 + if ok { 1025 + audioMetadata.Channels = channels 1026 + } 1027 + } 1028 + 1029 + if videoMetadata != nil && audioMetadata != nil { 1030 + cancel() 1031 + } 1032 + } 1033 + 1034 + demux, err := pipeline.GetElementByName("demux") 1035 + if err != nil { 1036 + return nil, fmt.Errorf("error creating SegmentMetadata pipeline: %w", err) 1037 + } 1038 + demux.Connect("pad-added", onPadAdded) 1039 + 1040 + go func() { 1041 + HandleBusMessages(ctx, pipeline) 1042 + cancel() 1043 + }() 1044 + 1045 + // Start the pipeline 1046 + pipeline.SetState(gst.StatePlaying) 1047 + 1048 + <-ctx.Done() 1049 + 1050 + meta := &model.SegmentMediaData{ 1051 + Video: []*model.SegmentMediadataVideo{videoMetadata}, 1052 + Audio: []*model.SegmentMediadataAudio{audioMetadata}, 1053 + } 1054 + 1055 + pipeline.BlockSetState(gst.StateNull) 1056 + 1057 + return meta, nil 1058 + }
+116 -123
pkg/media/m3u8.go
··· 6 6 "fmt" 7 7 "math" 8 8 "strings" 9 - "sync" 10 9 "time" 11 10 12 11 "github.com/google/uuid" 13 12 "stream.place/streamplace/pkg/log" 14 - "stream.place/streamplace/pkg/renditions" 15 13 ) 16 14 17 15 // how many segments are served in the live playlist? ··· 19 17 20 18 // how long should we keep old segments around? 21 19 const RETAIN_SEGMENT_SIZE = LIVE_PLAYLIST_SIZE * 3 22 - 23 - const INDEX_M3U8 = "index.m3u8" 24 20 25 21 type Segment struct { 26 - MSN uint64 // media sequence number 27 - Duration time.Duration 28 - Buf *bytes.Buffer 29 - Time time.Time 30 - Closed bool 31 - StartTS *uint64 32 - EndTS *uint64 22 + MSN uint64 // media sequence number 23 + Buf *bytes.Buffer 24 + StartTime *uint64 25 + EndTime *uint64 26 + Closed bool 27 + } 28 + 29 + func (s *Segment) Duration() time.Duration { 30 + return time.Duration(*s.EndTime - *s.StartTime) 33 31 } 34 32 35 33 type M3U8 struct { 36 34 curSeg uint64 35 + segments []*Segment 37 36 pendingSegments []*Segment 38 37 waits []chan struct{} 39 - renditions []*M3U8Rendition 38 + Bitrate uint64 39 + Width uint64 40 + Height uint64 40 41 } 41 42 42 - type M3U8Rendition struct { 43 - Rendition renditions.Rendition 44 - Segments []*Segment 45 - SegmentLock sync.RWMutex 46 - MSN uint64 43 + func NewM3U8() *M3U8 { 44 + return &M3U8{ 45 + curSeg: 0, 46 + } 47 47 } 48 48 49 - func NewM3U8(renditions renditions.Renditions) *M3U8 { 50 - rends := []*M3U8Rendition{} 51 - for _, r := range renditions { 52 - mr := &M3U8Rendition{ 53 - Rendition: r, 49 + func (m *M3U8) GetNextSegment(ctx context.Context) (*Segment, error) { 50 + log.Debug(ctx, "next segment") 51 + msn := m.curSeg 52 + m.curSeg += 1 53 + seg := &Segment{ 54 + MSN: msn, 55 + Buf: &bytes.Buffer{}, 56 + } 57 + m.pendingSegments = append(m.pendingSegments, seg) 58 + return seg, nil 59 + } 60 + 61 + func (m *M3U8) CloseSegment(ctx context.Context, seg *Segment) { 62 + log.Debug(ctx, "close segment", "MSN", seg.MSN) 63 + seg.Closed = true 64 + m.checkSegments(ctx) 65 + } 66 + 67 + func (m *M3U8) FragmentOpened(ctx context.Context, t uint64) error { 68 + log.Debug(ctx, "fragment opened", "time", t) 69 + if len(m.pendingSegments) == 0 { 70 + return fmt.Errorf("no pending segments") 71 + } 72 + for _, seg := range m.pendingSegments { 73 + if seg.StartTime == nil { 74 + seg.StartTime = &t 75 + break 54 76 } 55 - rends = append(rends, mr) 56 77 } 57 - return &M3U8{ 58 - curSeg: 0, 59 - renditions: rends, 78 + m.checkSegments(ctx) 79 + return nil 80 + } 81 + 82 + func (m *M3U8) FragmentClosed(ctx context.Context, t uint64) error { 83 + log.Debug(ctx, "fragment closed", "time", t) 84 + if len(m.pendingSegments) == 0 { 85 + return fmt.Errorf("no pending segments") 60 86 } 87 + for _, seg := range m.pendingSegments { 88 + if seg.EndTime == nil { 89 + seg.EndTime = &t 90 + if m.Bitrate == 0 { 91 + dur := seg.Duration() 92 + m.Bitrate = uint64(float64(seg.Buf.Len())/dur.Seconds()) * 8 93 + } 94 + break 95 + } 96 + } 97 + m.checkSegments(ctx) 98 + return nil 61 99 } 62 100 63 - func (r *M3U8Rendition) GetMediaLine(session string) string { 64 - // m.waitForStart() 65 - lines := []string{} 66 - lines = append(lines, "#EXTM3U") 67 - lines = append(lines, fmt.Sprintf("#EXT-X-STREAM-INF:BANDWIDTH=%d,RESOLUTION=%dx%d", r.Rendition.Bitrate, r.Rendition.Width, r.Rendition.Height)) 68 - lines = append(lines, fmt.Sprintf("%s/%s?session=%s", r.Rendition.Name, INDEX_M3U8, session)) 69 - return strings.Join(lines, "\n") 101 + // the tricky piece of the design here is that we need to expect GetNextSegment, 102 + // CloseSegment, FragmentOpened, and FragmentClosed to be called in any order. So 103 + // all of those functions call this one, and it checks if we have the necessary information 104 + // to finalize a segment and add it to our playlist. 105 + func (m *M3U8) checkSegments(ctx context.Context) { 106 + pending := m.pendingSegments[0] 107 + if pending.StartTime != nil && pending.EndTime != nil && pending.Closed { 108 + m.segments = append(m.segments, pending) 109 + m.pendingSegments = m.pendingSegments[1:] 110 + log.Debug(ctx, "finalizing segment", "MSN", pending.MSN) 111 + for _, wait := range m.waits { 112 + go func(wait chan struct{}) { 113 + wait <- struct{}{} 114 + }(wait) 115 + } 116 + m.waits = []chan struct{}{} 117 + } 118 + if len(m.segments) > RETAIN_SEGMENT_SIZE { 119 + startWith := len(m.segments) - RETAIN_SEGMENT_SIZE 120 + m.segments = m.segments[startWith:] 121 + } 70 122 } 71 123 72 - func (r *M3U8Rendition) GetPlaylist(session string) []byte { 73 - if session == "" { 74 - uu, err := uuid.NewV7() 75 - if err != nil { 76 - panic(err) 77 - } 78 - session = uu.String() 124 + func (m *M3U8) waitForStart() { 125 + if len(m.segments) == 0 { 126 + // todo: fix concurrent access here 127 + wait := make(chan struct{}) 128 + m.waits = append(m.waits, wait) 129 + <-wait 79 130 } 80 - r.SegmentLock.RLock() 81 - defer r.SegmentLock.RUnlock() 82 - // m.waitForStart() 131 + } 132 + 133 + func (m *M3U8) GetPlaylist(session string) []byte { 134 + m.waitForStart() 83 135 lines := []string{} 84 136 lines = append(lines, "#EXTM3U") 85 137 lines = append(lines, "#EXT-X-VERSION:3") 86 - startWith := len(r.Segments) - LIVE_PLAYLIST_SIZE 138 + startWith := len(m.segments) - LIVE_PLAYLIST_SIZE 87 139 if startWith < 0 { 88 140 startWith = 0 89 141 } 90 - if len(r.Segments) == 0 { 91 - return []byte{} 92 - } 93 - firstSeg := r.Segments[startWith] 94 - lastSeg := r.Segments[len(r.Segments)-1] 95 - targetDuration := int64(math.Round(lastSeg.Duration.Seconds())) 142 + firstSeg := m.segments[startWith] 143 + lastSeg := m.segments[len(m.segments)-1] 144 + targetDuration := int64(math.Round(lastSeg.Duration().Seconds())) 96 145 lines = append(lines, fmt.Sprintf("#EXT-X-MEDIA-SEQUENCE:%d", firstSeg.MSN)) 97 - lines = append(lines, fmt.Sprintf("#EXT-X-DISCONTINUITY-SEQUENCE:%d", firstSeg.MSN)) 98 - lines = append(lines, fmt.Sprintf("#EXT-X-TARGETDURATION:%d", targetDuration+1)) 99 - lines = append(lines, "#EXT-X-INDEPENDENT-SEGMENTS") 146 + lines = append(lines, fmt.Sprintf("#EXT-X-TARGETDURATION:%d", targetDuration)) 100 147 lines = append(lines, "") 101 - lastSegments := r.Segments[startWith:] 148 + lastSegments := m.segments[startWith:] 102 149 for _, seg := range lastSegments { 103 - dur := seg.Duration 104 - lines = append(lines, "#EXT-X-DISCONTINUITY") 105 - lines = append(lines, fmt.Sprintf("#EXT-X-PROGRAM-DATE-TIME:%s", seg.Time.Format(time.RFC3339Nano))) 150 + dur := seg.Duration() 106 151 lines = append(lines, fmt.Sprintf("#EXTINF:%f,", dur.Seconds())) 107 152 lines = append(lines, fmt.Sprintf("segment%05d.ts?session=%s", seg.MSN, session)) 108 153 } ··· 110 155 return []byte(strings.Join(lines, "\n")) 111 156 } 112 157 113 - func (r *M3U8Rendition) GetSegment(session string, filename string) []byte { 114 - r.SegmentLock.RLock() 115 - defer r.SegmentLock.RUnlock() 116 - for _, seg := range r.Segments { 117 - if fmt.Sprintf("segment%05d.ts", seg.MSN) == filename { 118 - return seg.Buf.Bytes() 119 - } 120 - } 121 - return nil 122 - } 123 - 124 - func (m *M3U8) GetMultivariantPlaylist(rendition string) []byte { 158 + func (m *M3U8) GetMultivariantPlaylist() []byte { 125 159 uu, err := uuid.NewV7() 126 160 if err != nil { 127 161 panic(err) 128 162 } 129 - // m.waitForStart() 163 + m.waitForStart() 130 164 lines := []string{} 131 165 lines = append(lines, "#EXTM3U") 132 - for _, r := range m.renditions { 133 - if rendition == "" || r.Rendition.Name == rendition { 134 - lines = append(lines, r.GetMediaLine(uu.String())) 135 - } 136 - } 166 + lines = append(lines, fmt.Sprintf("#EXT-X-STREAM-INF:BANDWIDTH=%d,RESOLUTION=%dx%d", m.Bitrate, m.Width, m.Height)) 167 + lines = append(lines, fmt.Sprintf("media.m3u8?session=%s", uu.String())) 137 168 return []byte(strings.Join(lines, "\n")) 138 169 } 139 170 140 - // needs to handle: 141 - // - index.m3u8 142 - // - 720p/stream.m3u8 143 - // - 720p/segment00015.ts 144 - func (m *M3U8) GetFile(str string, session string, rendition string) ([]byte, error) { 145 - str = strings.TrimPrefix(str, "/") 146 - if str == INDEX_M3U8 { 147 - return m.GetMultivariantPlaylist(rendition), nil 148 - } 149 - parts := strings.Split(str, "/") 150 - if len(parts) != 2 { 151 - return nil, fmt.Errorf("invalid path") 171 + // takes segment00015.ts and returns the corresponding segment 172 + func (m *M3U8) GetSegment(str string, session string) ([]byte, error) { 173 + if str == "stream.m3u8" { 174 + return m.GetMultivariantPlaylist(), nil 152 175 } 153 - rStr := parts[0] 154 - fStr := parts[1] 155 - rend := m.GetRendition(rStr) 156 - log.Debug(context.Background(), "m3u8 get file", "str", str, "session", session, "rend", rStr, "file", fStr) 157 - if rend == nil { 158 - return nil, fmt.Errorf("rendition not found") 176 + if str == "media.m3u8" { 177 + return m.GetPlaylist(session), nil 159 178 } 160 - if fStr == INDEX_M3U8 { 161 - return rend.GetPlaylist(session), nil 162 - } 163 - seg := rend.GetSegment(session, fStr) 164 - if seg == nil { 165 - return nil, fmt.Errorf("segment not found") 166 - } 167 - return seg, nil 168 - } 169 - 170 - func (r *M3U8Rendition) NewSegment(seg *Segment) error { 171 - r.SegmentLock.Lock() 172 - defer r.SegmentLock.Unlock() 173 - seg.MSN = r.MSN 174 - r.MSN += 1 175 - r.Segments = append(r.Segments, seg) 176 - if len(r.Segments) > RETAIN_SEGMENT_SIZE { 177 - // Calculate how many segments to remove 178 - removeCount := len(r.Segments) - RETAIN_SEGMENT_SIZE 179 - // Remove the oldest segments (from the front of the slice) 180 - r.Segments = r.Segments[removeCount:] 181 - } 182 - return nil 183 - } 184 - 185 - func (m *M3U8) GetRendition(rendition string) *M3U8Rendition { 186 - for _, r := range m.renditions { 187 - if r.Rendition.Name == rendition { 188 - return r 179 + for _, seg := range m.segments { 180 + if fmt.Sprintf("segment%05d.ts", seg.MSN) == str { 181 + return seg.Buf.Bytes(), nil 189 182 } 190 183 } 191 - return nil 184 + return nil, fmt.Errorf("segment not found") 192 185 }
+241 -9
pkg/media/media.go
··· 1 1 package media 2 2 3 3 import ( 4 + "bytes" 4 5 "context" 5 6 "crypto" 6 7 "encoding/json" 7 8 "errors" 8 9 "fmt" 9 10 "io" 11 + "strings" 10 12 "sync" 11 13 12 14 "github.com/go-gst/go-gst/gst" 13 15 "github.com/google/uuid" 16 + "github.com/livepeer/lpms/ffmpeg" 17 + "golang.org/x/sync/errgroup" 14 18 "stream.place/streamplace/pkg/aqtime" 15 19 "stream.place/streamplace/pkg/atproto" 16 20 "stream.place/streamplace/pkg/bus" 17 21 "stream.place/streamplace/pkg/config" 18 - "stream.place/streamplace/pkg/media/segchanman" 22 + "stream.place/streamplace/pkg/constants" 23 + "stream.place/streamplace/pkg/crypto/signers" 24 + "stream.place/streamplace/pkg/log" 19 25 "stream.place/streamplace/pkg/model" 20 26 21 27 "stream.place/streamplace/pkg/replication" 22 28 29 + "git.stream.place/streamplace/c2pa-go/pkg/c2pa" 23 30 "git.stream.place/streamplace/c2pa-go/pkg/c2pa/generated/manifeststore" 24 31 "github.com/piprate/json-gold/ld" 25 32 ) ··· 31 38 32 39 type MediaManager struct { 33 40 cli *config.CLI 34 - segChanMan *segchanman.SegChanMan 41 + mp4subs map[string][]chan string 42 + mp4subsmut sync.Mutex 35 43 replicator replication.Replicator 36 44 hlsRunning map[string]*M3U8 37 45 hlsRunningMut sync.Mutex ··· 63 71 } 64 72 return &MediaManager{ 65 73 cli: cli, 66 - segChanMan: segchanman.MakeSegChanMan(), 74 + mp4subs: map[string][]chan string{}, 67 75 replicator: rep, 68 76 hlsRunning: map[string]*M3U8{}, 69 77 httpPipes: map[string]io.Writer{}, ··· 108 116 } 109 117 110 118 // subscribe to the latest segments from a given user for livestreaming purposes 111 - func (mm *MediaManager) SubscribeSegment(ctx context.Context, user string, rendition string) <-chan *segchanman.Seg { 112 - return mm.segChanMan.SubscribeSegment(ctx, user, rendition) 119 + func (mm *MediaManager) SubscribeSegment(ctx context.Context, user string) <-chan string { 120 + mm.mp4subsmut.Lock() 121 + defer mm.mp4subsmut.Unlock() 122 + _, ok := mm.mp4subs[user] 123 + if !ok { 124 + mm.mp4subs[user] = []chan string{} 125 + } 126 + c := make(chan string) 127 + mm.mp4subs[user] = append(mm.mp4subs[user], c) 128 + return c 113 129 } 114 130 115 - func (mm *MediaManager) UnsubscribeSegment(ctx context.Context, user string, rendition string, ch <-chan *segchanman.Seg) { 116 - mm.segChanMan.UnsubscribeSegment(ctx, user, rendition, ch) 131 + func (mm *MediaManager) UnsubscribeSegment(ctx context.Context, user string, ch <-chan string) { 132 + mm.mp4subsmut.Lock() 133 + defer mm.mp4subsmut.Unlock() 134 + for i, c := range mm.mp4subs[user] { 135 + if c == ch { 136 + mm.mp4subs[user] = append(mm.mp4subs[user][:i], mm.mp4subs[user][i+1:]...) 137 + break 138 + } 139 + } 117 140 } 118 141 119 142 // subscribe to the latest segments from a given user for livestreaming purposes 120 - func (mm *MediaManager) PublishSegment(ctx context.Context, user, rendition string, seg *segchanman.Seg) { 121 - mm.segChanMan.PublishSegment(ctx, user, rendition, seg) 143 + func (mm *MediaManager) PublishSegment(ctx context.Context, user, file string) { 144 + mm.mp4subsmut.Lock() 145 + defer mm.mp4subsmut.Unlock() 146 + for _, sub := range mm.mp4subs[user] { 147 + go func() { 148 + sub <- file 149 + }() 150 + } 151 + mm.mp4subs[user] = []chan string{} 152 + } 153 + 154 + func (mm *MediaManager) SegmentToMKV(ctx context.Context, user string, w io.Writer) error { 155 + muxer := ffmpeg.ComponentOptions{ 156 + Name: "matroska", 157 + } 158 + return mm.SegmentToStream(ctx, user, muxer, w) 159 + } 160 + 161 + func (mm *MediaManager) SegmentToMKVPlusOpus(ctx context.Context, user string, w io.Writer) error { 162 + muxer := ffmpeg.ComponentOptions{ 163 + Name: "matroska", 164 + } 165 + pr, pw := io.Pipe() 166 + g, ctx := errgroup.WithContext(ctx) 167 + g.Go(func() error { 168 + return mm.SegmentToStream(ctx, user, muxer, pw) 169 + }) 170 + g.Go(func() error { 171 + return AddOpusToMKV(ctx, pr, w) 172 + }) 173 + return g.Wait() 174 + } 175 + 176 + func (mm *MediaManager) SegmentToHLSOnce(ctx context.Context, user string) (*M3U8, error) { 177 + mm.hlsRunningMut.Lock() 178 + defer mm.hlsRunningMut.Unlock() 179 + hls, ok := mm.hlsRunning[user] 180 + if !ok { 181 + hls = NewM3U8() 182 + mm.hlsRunning[user] = hls 183 + go func() { 184 + err := mm.SegmentToHLS(ctx, user, hls) 185 + if err != nil { 186 + log.Log(ctx, "error in async segmentToHLS code", "error", err) 187 + } 188 + mm.hlsRunningMut.Lock() 189 + defer mm.hlsRunningMut.Unlock() 190 + delete(mm.hlsRunning, user) 191 + }() 192 + } 193 + return hls, nil 194 + } 195 + 196 + func (mm *MediaManager) SegmentToHLS(ctx context.Context, user string, m3u8 *M3U8) error { 197 + muxer := ffmpeg.ComponentOptions{ 198 + Name: "matroska", 199 + } 200 + 201 + pr, pw := io.Pipe() 202 + g, ctx := errgroup.WithContext(ctx) 203 + g.Go(func() error { 204 + return mm.SegmentToStream(ctx, user, muxer, pw) 205 + }) 206 + g.Go(func() error { 207 + return mm.ToHLS(ctx, pr, m3u8) 208 + }) 209 + return g.Wait() 210 + } 211 + 212 + func (mm *MediaManager) SegmentToMP4(ctx context.Context, user string, w io.Writer) error { 213 + muxer := ffmpeg.ComponentOptions{ 214 + Name: "mp4", 215 + Opts: map[string]string{ 216 + "movflags": "frag_keyframe+empty_moov", 217 + }, 218 + } 219 + return mm.SegmentToStream(ctx, user, muxer, w) 220 + } 221 + 222 + func (mm *MediaManager) SegmentToStream(ctx context.Context, user string, muxer ffmpeg.ComponentOptions, w io.Writer) error { 223 + tc := ffmpeg.NewTranscoder() 224 + defer tc.StopTranscoder() 225 + ourl, or, odone, err := mm.HTTPPipe() 226 + if err != nil { 227 + return err 228 + } 229 + defer odone() 230 + iname := fmt.Sprintf("%s/playback/%s/concat", mm.cli.OwnInternalURL(), user) 231 + in := &ffmpeg.TranscodeOptionsIn{ 232 + Fname: iname, 233 + Transmuxing: true, 234 + Profile: ffmpeg.VideoProfile{}, 235 + Loop: -1, 236 + Demuxer: ffmpeg.ComponentOptions{ 237 + Name: "concat", 238 + Opts: map[string]string{ 239 + "safe": "0", 240 + "protocol_whitelist": "file,http,https,tcp,tls", 241 + }, 242 + }, 243 + } 244 + out := []ffmpeg.TranscodeOptions{ 245 + { 246 + Oname: ourl, 247 + VideoEncoder: ffmpeg.ComponentOptions{ 248 + Name: "copy", 249 + }, 250 + AudioEncoder: ffmpeg.ComponentOptions{ 251 + Name: "copy", 252 + }, 253 + Profile: ffmpeg.VideoProfile{Format: ffmpeg.FormatNone}, 254 + Muxer: muxer, 255 + }, 256 + } 257 + g, _ := errgroup.WithContext(ctx) 258 + g.Go(func() error { 259 + <-ctx.Done() 260 + or.Close() 261 + return nil 262 + }) 263 + g.Go(func() error { 264 + _, err := tc.Transcode(in, out) 265 + tc.StopTranscoder() 266 + return err 267 + }) 268 + g.Go(func() error { 269 + _, err := io.Copy(w, or) 270 + return err 271 + }) 272 + return g.Wait() 122 273 } 123 274 124 275 type obj map[string]any ··· 191 342 } 192 343 return &out, nil 193 344 } 345 + 346 + func (mm *MediaManager) ValidateMP4(ctx context.Context, input io.Reader) error { 347 + buf, err := io.ReadAll(input) 348 + if err != nil { 349 + return err 350 + } 351 + r := bytes.NewReader(buf) 352 + reader, err := c2pa.FromStream(r, "video/mp4") 353 + if err != nil { 354 + return err 355 + } 356 + mani := reader.GetActiveManifest() 357 + certs := reader.GetProvenanceCertChain() 358 + pub, err := signers.ParseES256KCert([]byte(certs)) 359 + if err != nil { 360 + return err 361 + } 362 + meta, err := ParseSegmentAssertions(mani) 363 + if err != nil { 364 + return err 365 + } 366 + mediaData, err := mm.ParseSegmentMediaData(ctx, buf) 367 + if err != nil { 368 + return err 369 + } 370 + // special case for test signers that are only signed with a key 371 + var repoDID string 372 + var signingKeyDID string 373 + if strings.HasPrefix(meta.Creator, constants.DID_KEY_PREFIX) { 374 + signingKeyDID = meta.Creator 375 + repoDID = meta.Creator 376 + } else { 377 + repo, err := mm.atsync.SyncBlueskyRepoCached(ctx, meta.Creator, mm.model) 378 + if err != nil { 379 + return err 380 + } 381 + signingKey, err := mm.model.GetSigningKey(pub.DIDKey(), repo.DID) 382 + if err != nil { 383 + return err 384 + } 385 + if signingKey == nil { 386 + return fmt.Errorf("no signing key found for %s", pub.DIDKey()) 387 + } 388 + repoDID = repo.DID 389 + signingKeyDID = signingKey.DID 390 + } 391 + 392 + err = mm.cli.StreamIsAllowed(repoDID) 393 + if err != nil { 394 + return fmt.Errorf("got valid segment, but user %s is not allowed: %w", repoDID, err) 395 + } 396 + fd, err := mm.cli.SegmentFileCreate(repoDID, meta.StartTime, "mp4") 397 + if err != nil { 398 + return err 399 + } 400 + defer fd.Close() 401 + go mm.replicator.NewSegment(ctx, buf) 402 + r = bytes.NewReader(buf) 403 + io.Copy(fd, r) 404 + go mm.PublishSegment(ctx, repoDID, fd.Name()) 405 + seg := &model.Segment{ 406 + ID: *mani.Label, 407 + SigningKeyDID: signingKeyDID, 408 + RepoDID: repoDID, 409 + StartTime: meta.StartTime.Time(), 410 + Title: meta.Title, 411 + MediaData: mediaData, 412 + } 413 + mm.newSegmentSubsMutex.RLock() 414 + defer mm.newSegmentSubsMutex.RUnlock() 415 + not := &NewSegmentNotification{ 416 + Segment: seg, 417 + Data: buf, 418 + Metadata: meta, 419 + } 420 + for _, ch := range mm.newSegmentSubs { 421 + go func() { ch <- not }() 422 + } 423 + log.Log(ctx, "successfully ingested segment", "user", repoDID, "signingKey", signingKeyDID, "timestamp", meta.StartTime, "segmentID", *mani.Label) 424 + return nil 425 + }
-140
pkg/media/media_data_parser.go
··· 1 - package media 2 - 3 - import ( 4 - "bytes" 5 - "context" 6 - "fmt" 7 - "strconv" 8 - "strings" 9 - 10 - "github.com/go-gst/go-gst/gst" 11 - "github.com/go-gst/go-gst/gst/app" 12 - "stream.place/streamplace/pkg/log" 13 - "stream.place/streamplace/pkg/model" 14 - ) 15 - 16 - func (mm *MediaManager) ParseSegmentMediaData(ctx context.Context, mp4bs []byte) (*model.SegmentMediaData, error) { 17 - ctx = log.WithLogValues(ctx, "GStreamerFunc", "ParseSegmentMediaData") 18 - ctx, cancel := context.WithCancel(ctx) 19 - defer cancel() 20 - pipelineSlice := []string{ 21 - "appsrc name=appsrc ! qtdemux name=demux ! fakesink", 22 - } 23 - 24 - pipeline, err := gst.NewPipelineFromString(strings.Join(pipelineSlice, "\n")) 25 - if err != nil { 26 - return nil, fmt.Errorf("error creating SegmentMetadata pipeline: %w", err) 27 - } 28 - 29 - var videoMetadata *model.SegmentMediadataVideo 30 - var audioMetadata *model.SegmentMediadataAudio 31 - 32 - appsrc, err := pipeline.GetElementByName("appsrc") 33 - if err != nil { 34 - return nil, fmt.Errorf("error creating SegmentMetadata pipeline: %w", err) 35 - } 36 - 37 - src := app.SrcFromElement(appsrc) 38 - src.SetCallbacks(&app.SourceCallbacks{ 39 - NeedDataFunc: ReaderNeedData(ctx, bytes.NewReader(mp4bs)), 40 - }) 41 - 42 - onPadAdded := func(element *gst.Element, pad *gst.Pad) { 43 - caps := pad.GetCurrentCaps() 44 - if caps == nil { 45 - log.Warn(ctx, "Unable to get pad caps") 46 - cancel() 47 - return 48 - } 49 - 50 - structure := caps.GetStructureAt(0) 51 - if structure == nil { 52 - log.Warn(ctx, "Unable to get structure from caps") 53 - cancel() 54 - return 55 - } 56 - 57 - name := structure.Name() 58 - 59 - if name[:5] == "video" { 60 - videoMetadata = &model.SegmentMediadataVideo{} 61 - // Get some common video properties 62 - widthVal, _ := structure.GetValue("width") 63 - heightVal, _ := structure.GetValue("height") 64 - 65 - width, ok := widthVal.(int) 66 - if ok { 67 - videoMetadata.Width = width 68 - } 69 - height, ok := heightVal.(int) 70 - if ok { 71 - videoMetadata.Height = height 72 - } 73 - framerateVal, _ := structure.GetValue("framerate") 74 - framerateStr := fmt.Sprintf("%v", framerateVal) 75 - parts := strings.Split(framerateStr, "/") 76 - num := 0 77 - den := 0 78 - if len(parts) == 2 { 79 - num, _ = strconv.Atoi(parts[0]) 80 - den, _ = strconv.Atoi(parts[1]) 81 - } 82 - if num != 0 && den != 0 { 83 - videoMetadata.FPSNum = num 84 - videoMetadata.FPSDen = den 85 - } 86 - } 87 - 88 - if name[:5] == "audio" { 89 - audioMetadata = &model.SegmentMediadataAudio{} 90 - // Get some common audio properties 91 - rateVal, _ := structure.GetValue("rate") 92 - channelsVal, _ := structure.GetValue("channels") 93 - 94 - rate, ok := rateVal.(int) 95 - if ok { 96 - audioMetadata.Rate = rate 97 - } 98 - channels, ok := channelsVal.(int) 99 - if ok { 100 - audioMetadata.Channels = channels 101 - } 102 - } 103 - 104 - // if videoMetadata != nil && audioMetadata != nil { 105 - // cancel() 106 - // } 107 - } 108 - 109 - demux, err := pipeline.GetElementByName("demux") 110 - if err != nil { 111 - return nil, fmt.Errorf("error creating SegmentMetadata pipeline: %w", err) 112 - } 113 - demux.Connect("pad-added", onPadAdded) 114 - 115 - go func() { 116 - HandleBusMessages(ctx, pipeline) 117 - cancel() 118 - }() 119 - 120 - // Start the pipeline 121 - pipeline.SetState(gst.StatePlaying) 122 - 123 - <-ctx.Done() 124 - 125 - meta := &model.SegmentMediaData{ 126 - Video: []*model.SegmentMediadataVideo{videoMetadata}, 127 - Audio: []*model.SegmentMediadataAudio{audioMetadata}, 128 - } 129 - 130 - ok, dur := pipeline.QueryDuration(gst.FormatTime) 131 - if !ok { 132 - return nil, fmt.Errorf("error getting duration") 133 - } else { 134 - meta.Duration = dur 135 - } 136 - 137 - pipeline.BlockSetState(gst.StateNull) 138 - 139 - return meta, nil 140 - }
-198
pkg/media/progressive.go
··· 1 - package media 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "io" 7 - "strings" 8 - "time" 9 - 10 - "github.com/go-gst/go-gst/gst" 11 - "github.com/go-gst/go-gst/gst/app" 12 - "github.com/google/uuid" 13 - "stream.place/streamplace/pkg/log" 14 - ) 15 - 16 - func (mm *MediaManager) MP4Playback(ctx context.Context, user string, rendition string, w io.Writer) error { 17 - uu, err := uuid.NewV7() 18 - if err != nil { 19 - return err 20 - } 21 - ctx = log.WithLogValues(ctx, "playbackID", uu.String()) 22 - ctx, cancel := context.WithCancel(ctx) 23 - 24 - ctx = log.WithLogValues(ctx, "mediafunc", "MP4Playback") 25 - 26 - pipelineSlice := []string{ 27 - "mp4mux name=muxer fragment-mode=first-moov-then-finalise fragment-duration=1000 streamable=true ! appsink name=mp4sink", 28 - "h264parse name=videoparse ! muxer.", 29 - "opusparse name=audioparse ! muxer.", 30 - } 31 - 32 - pipeline, err := gst.NewPipelineFromString(strings.Join(pipelineSlice, "\n")) 33 - if err != nil { 34 - return fmt.Errorf("failed to create GStreamer pipeline: %w", err) 35 - } 36 - 37 - go func() { 38 - HandleBusMessages(ctx, pipeline) 39 - cancel() 40 - }() 41 - 42 - outputQueue, done, err := ConcatStream(ctx, pipeline, user, rendition, mm) 43 - if err != nil { 44 - return fmt.Errorf("failed to get output queue: %w", err) 45 - } 46 - go func() { 47 - select { 48 - case <-ctx.Done(): 49 - return 50 - case <-done: 51 - cancel() 52 - } 53 - }() 54 - 55 - videoParse, err := pipeline.GetElementByName("videoparse") 56 - if err != nil { 57 - return fmt.Errorf("failed to get video sink element from pipeline: %w", err) 58 - } 59 - err = outputQueue.Link(videoParse) 60 - if err != nil { 61 - return fmt.Errorf("failed to link output queue to video parse: %w", err) 62 - } 63 - 64 - audioParse, err := pipeline.GetElementByName("audioparse") 65 - if err != nil { 66 - return fmt.Errorf("failed to get audio parse element from pipeline: %w", err) 67 - } 68 - err = outputQueue.Link(audioParse) 69 - if err != nil { 70 - return fmt.Errorf("failed to link output queue to audio parse: %w", err) 71 - } 72 - 73 - go func() { 74 - ticker := time.NewTicker(time.Second * 1) 75 - for { 76 - select { 77 - case <-ctx.Done(): 78 - return 79 - case <-ticker.C: 80 - state := pipeline.GetCurrentState() 81 - log.Debug(ctx, "pipeline state", "state", state) 82 - } 83 - } 84 - }() 85 - 86 - mp4sinkele, err := pipeline.GetElementByName("mp4sink") 87 - if err != nil { 88 - return fmt.Errorf("failed to get video sink element from pipeline: %w", err) 89 - } 90 - mp4sink := app.SinkFromElement(mp4sinkele) 91 - mp4sink.SetCallbacks(&app.SinkCallbacks{ 92 - NewSampleFunc: WriterNewSample(ctx, w), 93 - EOSFunc: func(sink *app.Sink) { 94 - log.Warn(ctx, "mp4sink EOSFunc") 95 - cancel() 96 - }, 97 - }) 98 - 99 - pipeline.SetState(gst.StatePlaying) 100 - 101 - <-ctx.Done() 102 - 103 - pipeline.BlockSetState(gst.StateNull) 104 - 105 - return nil 106 - } 107 - 108 - func (mm *MediaManager) MKVPlayback(ctx context.Context, user string, rendition string, w io.Writer) error { 109 - uu, err := uuid.NewV7() 110 - if err != nil { 111 - return err 112 - } 113 - ctx = log.WithLogValues(ctx, "playbackID", uu.String()) 114 - ctx, cancel := context.WithCancel(ctx) 115 - 116 - ctx = log.WithLogValues(ctx, "mediafunc", "MKVPlayback") 117 - 118 - pipelineSlice := []string{ 119 - "matroskamux name=muxer streamable=true ! appsink name=mkvsink", 120 - "h264parse name=videoparse ! muxer.", 121 - "opusparse name=audioparse ! muxer.", 122 - } 123 - 124 - pipeline, err := gst.NewPipelineFromString(strings.Join(pipelineSlice, "\n")) 125 - if err != nil { 126 - return fmt.Errorf("failed to create GStreamer pipeline: %w", err) 127 - } 128 - 129 - go func() { 130 - HandleBusMessages(ctx, pipeline) 131 - cancel() 132 - }() 133 - 134 - outputQueue, done, err := ConcatStream(ctx, pipeline, user, rendition, mm) 135 - if err != nil { 136 - return fmt.Errorf("failed to get output queue: %w", err) 137 - } 138 - go func() { 139 - select { 140 - case <-ctx.Done(): 141 - return 142 - case <-done: 143 - cancel() 144 - } 145 - }() 146 - 147 - videoParse, err := pipeline.GetElementByName("videoparse") 148 - if err != nil { 149 - return fmt.Errorf("failed to get video sink element from pipeline: %w", err) 150 - } 151 - err = outputQueue.Link(videoParse) 152 - if err != nil { 153 - return fmt.Errorf("failed to link output queue to video parse: %w", err) 154 - } 155 - 156 - audioParse, err := pipeline.GetElementByName("audioparse") 157 - if err != nil { 158 - return fmt.Errorf("failed to get audio parse element from pipeline: %w", err) 159 - } 160 - err = outputQueue.Link(audioParse) 161 - if err != nil { 162 - return fmt.Errorf("failed to link output queue to audio parse: %w", err) 163 - } 164 - 165 - go func() { 166 - ticker := time.NewTicker(time.Second * 1) 167 - for { 168 - select { 169 - case <-ctx.Done(): 170 - return 171 - case <-ticker.C: 172 - state := pipeline.GetCurrentState() 173 - log.Debug(ctx, "pipeline state", "state", state) 174 - } 175 - } 176 - }() 177 - 178 - mkvsinkele, err := pipeline.GetElementByName("mkvsink") 179 - if err != nil { 180 - return fmt.Errorf("failed to get video sink element from pipeline: %w", err) 181 - } 182 - mkvsink := app.SinkFromElement(mkvsinkele) 183 - mkvsink.SetCallbacks(&app.SinkCallbacks{ 184 - NewSampleFunc: WriterNewSample(ctx, w), 185 - EOSFunc: func(sink *app.Sink) { 186 - log.Warn(ctx, "mp4sink EOSFunc") 187 - cancel() 188 - }, 189 - }) 190 - 191 - pipeline.SetState(gst.StatePlaying) 192 - 193 - <-ctx.Done() 194 - 195 - pipeline.BlockSetState(gst.StateNull) 196 - 197 - return nil 198 - }
-76
pkg/media/segchanman/segchanman.go
··· 1 - package segchanman 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "sync" 7 - ) 8 - 9 - // it's a segment channel manager, you see 10 - 11 - type Seg struct { 12 - Filepath string 13 - Data []byte 14 - } 15 - 16 - type SegChanMan struct { 17 - segChans map[string][]chan *Seg 18 - segChansMutex sync.Mutex 19 - } 20 - 21 - func MakeSegChanMan() *SegChanMan { 22 - return &SegChanMan{ 23 - segChans: make(map[string][]chan *Seg), 24 - } 25 - } 26 - 27 - func segChanKey(user string, rendition string) string { 28 - return fmt.Sprintf("%s::%s", user, rendition) 29 - } 30 - 31 - func (s *SegChanMan) SubscribeSegment(ctx context.Context, user string, rendition string) <-chan *Seg { 32 - key := segChanKey(user, rendition) 33 - s.segChansMutex.Lock() 34 - defer s.segChansMutex.Unlock() 35 - chs, ok := s.segChans[key] 36 - if !ok { 37 - chs = []chan *Seg{} 38 - s.segChans[key] = chs 39 - } 40 - ch := make(chan *Seg, 1024) 41 - chs = append(chs, ch) 42 - s.segChans[key] = chs 43 - return ch 44 - } 45 - 46 - func (s *SegChanMan) UnsubscribeSegment(ctx context.Context, user string, rendition string, ch <-chan *Seg) { 47 - key := segChanKey(user, rendition) 48 - s.segChansMutex.Lock() 49 - defer s.segChansMutex.Unlock() 50 - chs, ok := s.segChans[key] 51 - if !ok { 52 - return 53 - } 54 - for i, c := range chs { 55 - if c == ch { 56 - chs = append(chs[:i], chs[i+1:]...) 57 - break 58 - } 59 - } 60 - s.segChans[key] = chs 61 - } 62 - 63 - func (s *SegChanMan) PublishSegment(ctx context.Context, user string, rendition string, seg *Seg) { 64 - key := segChanKey(user, rendition) 65 - s.segChansMutex.Lock() 66 - defer s.segChansMutex.Unlock() 67 - chs, ok := s.segChans[key] 68 - if !ok { 69 - return 70 - } 71 - for _, ch := range chs { 72 - go func(ch chan *Seg) { 73 - ch <- seg 74 - }(ch) 75 - } 76 - }
-131
pkg/media/segment_conv.go
··· 1 - package media 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "io" 7 - "strings" 8 - 9 - "github.com/go-gst/go-gst/gst" 10 - "github.com/go-gst/go-gst/gst/app" 11 - "golang.org/x/sync/errgroup" 12 - ) 13 - 14 - // MP4ToMPEGTS converts an MP4 file with H264 video and Opus audio to an MPEG-TS file with H264 video and AAC audio. 15 - // It reads from the provided reader and writes the converted MPEG-TS to the writer. 16 - func MP4ToMPEGTS(ctx context.Context, input io.Reader, output io.Writer) (int64, error) { 17 - pipelineStr := strings.Join([]string{ 18 - "appsrc name=appsrc ! qtdemux name=demux", 19 - "mpegtsmux name=mux ! appsink name=appsink", 20 - "demux.video_0 ! h264parse ! video/x-h264,stream-format=byte-stream ! queue name=videoqueue", 21 - "demux.audio_0 ! opusdec use-inband-fec=true ! audioresample ! fdkaacenc ! aacparse ! queue name=audioqueue", 22 - }, " ") 23 - 24 - pipeline, err := gst.NewPipelineFromString(pipelineStr) 25 - if err != nil { 26 - return 0, err 27 - } 28 - 29 - mux, err := pipeline.GetElementByName("mux") 30 - if err != nil { 31 - return 0, err 32 - } 33 - muxVideoSinkPad := mux.GetRequestPad("sink_%d") 34 - if muxVideoSinkPad == nil { 35 - return 0, fmt.Errorf("failed to get video sink pad") 36 - } 37 - muxAudioSinkPad := mux.GetRequestPad("sink_%d") 38 - if muxAudioSinkPad == nil { 39 - return 0, fmt.Errorf("failed to get audio sink pad") 40 - } 41 - videoQueue, err := pipeline.GetElementByName("videoqueue") 42 - if err != nil { 43 - return 0, err 44 - } 45 - audioQueue, err := pipeline.GetElementByName("audioqueue") 46 - if err != nil { 47 - return 0, err 48 - } 49 - videoQueueSrcPad := videoQueue.GetStaticPad("src") 50 - if videoQueueSrcPad == nil { 51 - return 0, fmt.Errorf("failed to get video queue source pad") 52 - } 53 - audioQueueSrcPad := audioQueue.GetStaticPad("src") 54 - if audioQueueSrcPad == nil { 55 - return 0, fmt.Errorf("failed to get audio queue source pad") 56 - } 57 - 58 - ok := videoQueueSrcPad.Link(muxVideoSinkPad) 59 - if ok != gst.PadLinkOK { 60 - return 0, fmt.Errorf("failed to link video queue source pad to mux video sink pad: %v", ok) 61 - } 62 - ok = audioQueueSrcPad.Link(muxAudioSinkPad) 63 - if ok != gst.PadLinkOK { 64 - return 0, fmt.Errorf("failed to link audio queue source pad to mux audio sink pad: %v", ok) 65 - } 66 - 67 - // Get elements 68 - appsrc, err := pipeline.GetElementByName("appsrc") 69 - if err != nil { 70 - return 0, err 71 - } 72 - appsink, err := pipeline.GetElementByName("appsink") 73 - if err != nil { 74 - return 0, err 75 - } 76 - 77 - source := app.SrcFromElement(appsrc) 78 - sink := app.SinkFromElement(appsink) 79 - 80 - // Set up source callbacks 81 - source.SetCallbacks(&app.SourceCallbacks{ 82 - NeedDataFunc: ReaderNeedData(ctx, input), 83 - EnoughDataFunc: func(self *app.Source) { 84 - // Nothing to do here 85 - }, 86 - SeekDataFunc: func(self *app.Source, offset uint64) bool { 87 - return false // We don't support seeking 88 - }, 89 - }) 90 - 91 - // Set up sink callbacks 92 - sink.SetCallbacks(&app.SinkCallbacks{ 93 - NewSampleFunc: WriterNewSample(ctx, output), 94 - NewPrerollFunc: func(self *app.Sink) gst.FlowReturn { 95 - return gst.FlowOK 96 - }, 97 - }) 98 - 99 - ctx, cancel := context.WithCancel(ctx) 100 - defer cancel() 101 - 102 - // Handle bus messages in a separate goroutine 103 - g, ctx := errgroup.WithContext(ctx) 104 - g.Go(func() error { 105 - HandleBusMessages(ctx, pipeline) 106 - cancel() 107 - return nil 108 - }) 109 - 110 - // Start the pipeline 111 - err = pipeline.SetState(gst.StatePlaying) 112 - if err != nil { 113 - return 0, fmt.Errorf("failed to set pipeline state to playing: %w", err) 114 - } 115 - 116 - // Wait for the pipeline to finish or context to be canceled 117 - <-ctx.Done() 118 - 119 - durOk, dur := pipeline.QueryDuration(gst.FormatTime) 120 - if !durOk { 121 - return 0, fmt.Errorf("failed to query duration") 122 - } 123 - 124 - // Clean up 125 - err = pipeline.SetState(gst.StateNull) 126 - if err != nil { 127 - return 0, fmt.Errorf("failed to set pipeline state to null: %w", err) 128 - } 129 - 130 - return dur, nil 131 - }
-35
pkg/media/segment_conv_test.go
··· 1 - package media 2 - 3 - import ( 4 - "context" 5 - "os" 6 - "testing" 7 - 8 - "github.com/go-gst/go-gst/gst" 9 - "github.com/stretchr/testify/require" 10 - ) 11 - 12 - func TestMP4ToMPEGTS(t *testing.T) { 13 - gst.Init(nil) 14 - 15 - // Open input file 16 - inputFile, err := os.Open(getFixture("sample-segment.mp4")) 17 - require.NoError(t, err) 18 - defer inputFile.Close() 19 - 20 - // Create temporary output file 21 - outputFile, err := os.CreateTemp("", "*.ts") 22 - require.NoError(t, err) 23 - defer os.Remove(outputFile.Name()) 24 - defer outputFile.Close() 25 - 26 - // Convert MP4 to MPEG-TS 27 - dur, err := MP4ToMPEGTS(context.Background(), inputFile, outputFile) 28 - require.NoError(t, err) 29 - require.Greater(t, dur, int64(0), "Duration should be greater than 0") 30 - 31 - // Verify output file has content 32 - info, err := os.Stat(outputFile.Name()) 33 - require.NoError(t, err) 34 - require.Greater(t, info.Size(), int64(0), "Output file should not be empty") 35 - }
-81
pkg/media/segmenter.go
··· 1 - package media 2 - 3 - import ( 4 - "bytes" 5 - "context" 6 - "fmt" 7 - "time" 8 - 9 - "github.com/go-gst/go-gst/gst" 10 - "github.com/go-gst/go-gst/gst/app" 11 - "stream.place/streamplace/pkg/log" 12 - ) 13 - 14 - // element that takes the input stream, muxes to mp4, and signs the result 15 - func (mm *MediaManager) SegmentAndSignElem(ctx context.Context, ms MediaSigner) (*gst.Element, error) { 16 - // elem, err := gst.NewElement("splitmuxsink name=splitter async-finalize=true sink-factory=appsink muxer-factory=matroskamux max-size-bytes=1") 17 - elem, err := gst.NewElementWithProperties("splitmuxsink", map[string]any{ 18 - "name": "signer", 19 - "async-finalize": true, 20 - "sink-factory": "appsink", 21 - "muxer-factory": "mp4mux", 22 - "max-size-bytes": 1, 23 - }) 24 - if err != nil { 25 - return nil, err 26 - } 27 - 28 - p := elem.GetRequestPad("video") 29 - if p == nil { 30 - return nil, fmt.Errorf("failed to get video pad") 31 - } 32 - p = elem.GetRequestPad("audio_%u") 33 - if p == nil { 34 - return nil, fmt.Errorf("failed to get audio pad") 35 - } 36 - 37 - resetTimer := make(chan struct{}) 38 - 39 - go func() { 40 - for { 41 - select { 42 - case <-ctx.Done(): 43 - return 44 - case <-resetTimer: 45 - continue 46 - case <-time.After(time.Second * 10): 47 - log.Warn(ctx, "no new segment for 10 seconds") 48 - elem.ErrorMessage(gst.DomainCore, gst.CoreErrorFailed, "No new segment for 10 seconds", "No new segment for 10 seconds (debug)") 49 - return 50 - } 51 - } 52 - }() 53 - 54 - elem.Connect("sink-added", func(split, sinkEle *gst.Element) { 55 - buf := &bytes.Buffer{} 56 - appsink := app.SinkFromElement(sinkEle) 57 - if appsink == nil { 58 - panic("appsink should not be nil") 59 - } 60 - appsink.SetCallbacks(&app.SinkCallbacks{ 61 - NewSampleFunc: WriterNewSample(ctx, buf), 62 - EOSFunc: func(sink *app.Sink) { 63 - resetTimer <- struct{}{} 64 - now := time.Now().UnixMilli() 65 - bs, err := ms.SignMP4(ctx, bytes.NewReader(buf.Bytes()), now) 66 - if err != nil { 67 - log.Error(ctx, "error signing segment", "error", err) 68 - return 69 - } 70 - 71 - err = mm.ValidateMP4(ctx, bytes.NewReader(bs)) 72 - if err != nil { 73 - log.Error(ctx, "error validating segment", "error", err) 74 - return 75 - } 76 - }, 77 - }) 78 - }) 79 - 80 - return elem, nil 81 - }
-296
pkg/media/segmenter_hls.go
··· 1 - package media 2 - 3 - import ( 4 - "bytes" 5 - "context" 6 - "fmt" 7 - "strings" 8 - "sync" 9 - "time" 10 - 11 - "github.com/go-gst/go-gst/gst" 12 - "github.com/go-gst/go-gst/gst/app" 13 - "stream.place/streamplace/pkg/log" 14 - ) 15 - 16 - func (mm *MediaManager) ToHLS(ctx context.Context, user string, rendition string, m3u8 *M3U8) error { 17 - ctx = log.WithLogValues(ctx, "GStreamerFunc", "ToHLS", "rendition", rendition) 18 - 19 - pipelineSlice := []string{ 20 - "h264parse name=videoparse", 21 - "opusdec use-inband-fec=true name=audioparse ! audioresample ! audiorate ! fdkaacenc name=audioenc", 22 - } 23 - 24 - pipeline, err := gst.NewPipelineFromString(strings.Join(pipelineSlice, "\n")) 25 - if err != nil { 26 - return fmt.Errorf("error creating ToHLS pipeline: %w", err) 27 - } 28 - 29 - outputQueue, done, err := ConcatStream(ctx, pipeline, user, rendition, mm) 30 - if err != nil { 31 - return fmt.Errorf("failed to get output queue: %w", err) 32 - } 33 - 34 - videoParse, err := pipeline.GetElementByName("videoparse") 35 - if err != nil { 36 - return fmt.Errorf("failed to get video sink element from pipeline: %w", err) 37 - } 38 - err = outputQueue.Link(videoParse) 39 - if err != nil { 40 - return fmt.Errorf("failed to link output queue to video parse: %w", err) 41 - } 42 - 43 - audioParse, err := pipeline.GetElementByName("audioparse") 44 - if err != nil { 45 - return fmt.Errorf("failed to get audio parse element from pipeline: %w", err) 46 - } 47 - err = outputQueue.Link(audioParse) 48 - if err != nil { 49 - return fmt.Errorf("failed to link output queue to audio parse: %w", err) 50 - } 51 - 52 - splitmuxsink, err := gst.NewElementWithProperties("splitmuxsink", map[string]any{ 53 - "name": "mux", 54 - "async-finalize": true, 55 - "sink-factory": "appsink", 56 - "muxer-factory": "mpegtsmux", 57 - "max-size-bytes": 1, 58 - }) 59 - if err != nil { 60 - return err 61 - } 62 - 63 - r := m3u8.GetRendition(rendition) 64 - defer func() { r = nil }() 65 - ps := NewPendingSegments(r) 66 - defer func() { ps = nil }() 67 - 68 - p := splitmuxsink.GetRequestPad("video") 69 - if p == nil { 70 - return fmt.Errorf("failed to get video pad") 71 - } 72 - p = splitmuxsink.GetRequestPad("audio_%u") 73 - if p == nil { 74 - return fmt.Errorf("failed to get audio pad") 75 - } 76 - 77 - err = pipeline.Add(splitmuxsink) 78 - if err != nil { 79 - return fmt.Errorf("error adding splitmuxsink to ToHLS pipeline: %w", err) 80 - } 81 - 82 - videoparse, err := pipeline.GetElementByName("videoparse") 83 - if err != nil { 84 - return fmt.Errorf("error getting videoparse from ToHLS pipeline: %w", err) 85 - } 86 - err = videoparse.Link(splitmuxsink) 87 - if err != nil { 88 - return fmt.Errorf("error linking videoparse to splitmuxsink: %w", err) 89 - } 90 - 91 - audioenc, err := pipeline.GetElementByName("audioenc") 92 - if err != nil { 93 - return fmt.Errorf("error getting audioenc from ToHLS pipeline: %w", err) 94 - } 95 - err = audioenc.Link(splitmuxsink) 96 - if err != nil { 97 - return fmt.Errorf("error linking audioenc to splitmuxsink: %w", err) 98 - } 99 - 100 - ctx, cancel := context.WithCancel(ctx) 101 - 102 - go func() { 103 - select { 104 - case <-ctx.Done(): 105 - return 106 - case <-done: 107 - cancel() 108 - } 109 - }() 110 - 111 - splitmuxsink.Connect("sink-added", func(split, sinkEle *gst.Element) { 112 - log.Debug(ctx, "hls-check sink-added") 113 - vf, err := ps.GetNextSegment(ctx) 114 - if err != nil { 115 - panic(err) 116 - } 117 - appsink := app.SinkFromElement(sinkEle) 118 - appsink.SetCallbacks(&app.SinkCallbacks{ 119 - NewSampleFunc: WriterNewSample(ctx, vf.Buf), 120 - EOSFunc: func(sink *app.Sink) { 121 - log.Debug(ctx, "hls-check Segment EOS", "buf", vf.Buf.Len()) 122 - ps.CloseSegment(ctx, vf) 123 - }, 124 - }) 125 - }) 126 - 127 - onPadAdded := func(element *gst.Element, pad *gst.Pad) { 128 - caps := pad.GetCurrentCaps() 129 - if caps == nil { 130 - fmt.Println("Unable to get pad caps") 131 - return 132 - } 133 - 134 - log.Debug(ctx, "New pad added", "pad", pad.GetName(), "caps", caps.String()) 135 - 136 - structure := caps.GetStructureAt(0) 137 - if structure == nil { 138 - fmt.Println("Unable to get structure from caps") 139 - return 140 - } 141 - 142 - name := structure.Name() 143 - fmt.Printf("Structure Name: %s\n", name) 144 - 145 - if name[:5] == "video" { 146 - // Get some common video properties 147 - // widthVal, _ := structure.GetValue("width") 148 - // heightVal, _ := structure.GetValue("height") 149 - 150 - // width, ok := widthVal.(int) 151 - // if ok { 152 - // m3u8.Width = uint64(width) 153 - // } 154 - // height, ok := heightVal.(int) 155 - // if ok { 156 - // m3u8.Height = uint64(height) 157 - // } 158 - } 159 - } 160 - 161 - splitmuxsink.Connect("pad-added", onPadAdded) 162 - 163 - defer cancel() 164 - go func() { 165 - HandleBusMessagesCustom(ctx, pipeline, func(msg *gst.Message) { 166 - switch msg.Type() { 167 - case gst.MessageElement: 168 - structure := msg.GetStructure() 169 - name := structure.Name() 170 - if name == "splitmuxsink-fragment-opened" { 171 - runningTime, err := structure.GetValue("running-time") 172 - if err != nil { 173 - log.Debug(ctx, "splitmuxsink-fragment-opened error", "error", err) 174 - cancel() 175 - } 176 - runningTimeInt, ok := runningTime.(uint64) 177 - if !ok { 178 - log.Warn(ctx, "splitmuxsink-fragment-opened not a uint64") 179 - cancel() 180 - } 181 - log.Debug(ctx, "hls-check splitmuxsink-fragment-opened", "runningTime", runningTimeInt) 182 - ps.FragmentOpened(ctx, runningTimeInt) 183 - } 184 - if name == "splitmuxsink-fragment-closed" { 185 - runningTime, err := structure.GetValue("running-time") 186 - if err != nil { 187 - log.Debug(ctx, "splitmuxsink-fragment-closed error", "error", err) 188 - cancel() 189 - } 190 - runningTimeInt, ok := runningTime.(uint64) 191 - if !ok { 192 - log.Warn(ctx, "splitmuxsink-fragment-closed not a uint64") 193 - cancel() 194 - } 195 - log.Debug(ctx, "hls-check splitmuxsink-fragment-closed", "runningTime", runningTimeInt) 196 - ps.FragmentClosed(ctx, runningTimeInt) 197 - } 198 - } 199 - }) 200 - cancel() 201 - }() 202 - 203 - // Start the pipeline 204 - pipeline.SetState(gst.StatePlaying) 205 - 206 - <-ctx.Done() 207 - 208 - pipeline.BlockSetState(gst.StateNull) 209 - 210 - return nil 211 - } 212 - 213 - type PendingSegments struct { 214 - segments []*Segment 215 - lock sync.Mutex 216 - rendition *M3U8Rendition 217 - } 218 - 219 - func NewPendingSegments(rendition *M3U8Rendition) *PendingSegments { 220 - return &PendingSegments{ 221 - segments: []*Segment{}, 222 - lock: sync.Mutex{}, 223 - rendition: rendition, 224 - } 225 - } 226 - 227 - func (ps *PendingSegments) GetNextSegment(ctx context.Context) (*Segment, error) { 228 - ps.lock.Lock() 229 - defer ps.lock.Unlock() 230 - log.Debug(ctx, "next segment") 231 - seg := &Segment{ 232 - Buf: &bytes.Buffer{}, 233 - Time: time.Now(), 234 - Closed: false, 235 - } 236 - ps.segments = append(ps.segments, seg) 237 - return seg, nil 238 - } 239 - 240 - func (ps *PendingSegments) CloseSegment(ctx context.Context, seg *Segment) { 241 - ps.lock.Lock() 242 - defer ps.lock.Unlock() 243 - log.Debug(ctx, "close segment", "MSN", seg.MSN) 244 - seg.Closed = true 245 - ps.checkSegments(ctx) 246 - } 247 - 248 - func (ps *PendingSegments) FragmentOpened(ctx context.Context, t uint64) error { 249 - ps.lock.Lock() 250 - defer ps.lock.Unlock() 251 - log.Debug(ctx, "fragment opened", "time", t) 252 - if len(ps.segments) == 0 { 253 - return fmt.Errorf("no pending segments") 254 - } 255 - for _, seg := range ps.segments { 256 - if seg.StartTS == nil { 257 - seg.StartTS = &t 258 - break 259 - } 260 - } 261 - ps.checkSegments(ctx) 262 - return nil 263 - } 264 - 265 - func (ps *PendingSegments) FragmentClosed(ctx context.Context, t uint64) error { 266 - ps.lock.Lock() 267 - defer ps.lock.Unlock() 268 - log.Debug(ctx, "fragment closed", "time", t) 269 - if len(ps.segments) == 0 { 270 - return fmt.Errorf("no pending segments") 271 - } 272 - for _, seg := range ps.segments { 273 - if seg.EndTS == nil { 274 - seg.EndTS = &t 275 - dur := *seg.EndTS - *seg.StartTS 276 - seg.Duration = time.Duration(dur) 277 - break 278 - } 279 - } 280 - ps.checkSegments(ctx) 281 - return nil 282 - } 283 - 284 - // the tricky piece of the design here is that we need to expect GetNextSegment, 285 - // CloseSegment, FragmentOpened, and FragmentClosed to be called in any order. So 286 - // all of those functions call this one, and it checks if we have the necessary information 287 - // to finalize a segment and add it to our playlist. 288 - // only call if you're holding ps.lock! 289 - func (ps *PendingSegments) checkSegments(ctx context.Context) { 290 - pending := ps.segments[0] 291 - if pending.StartTS != nil && pending.EndTS != nil && pending.Closed { 292 - ps.rendition.NewSegment(pending) 293 - log.Debug(ctx, "finalizing segment", "MSN", pending.MSN) 294 - ps.segments = ps.segments[1:] 295 - } 296 - }
-62
pkg/media/thumbnail.go
··· 1 - package media 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "io" 7 - "strings" 8 - 9 - "github.com/go-gst/go-gst/gst" 10 - "github.com/go-gst/go-gst/gst/app" 11 - "stream.place/streamplace/pkg/log" 12 - ) 13 - 14 - func (mm *MediaManager) Thumbnail(ctx context.Context, r io.Reader, w io.Writer) error { 15 - ctx = log.WithLogValues(ctx, "function", "Thumbnail") 16 - ctx, cancel := context.WithCancel(ctx) 17 - defer cancel() 18 - 19 - pipelineSlice := []string{ 20 - "appsrc name=appsrc ! qtdemux ! decodebin ! videoconvert ! videoscale ! video/x-raw,width=[1,720],height=[1,720],pixel-aspect-ratio=1/1 ! pngenc snapshot=true ! appsink name=appsink", 21 - } 22 - 23 - pipeline, err := gst.NewPipelineFromString(strings.Join(pipelineSlice, "\n")) 24 - if err != nil { 25 - return fmt.Errorf("error creating Thumbnail pipeline: %w", err) 26 - } 27 - appsrc, err := pipeline.GetElementByName("appsrc") 28 - if err != nil { 29 - return err 30 - } 31 - 32 - src := app.SrcFromElement(appsrc) 33 - src.SetCallbacks(&app.SourceCallbacks{ 34 - NeedDataFunc: ReaderNeedData(ctx, r), 35 - }) 36 - 37 - appsink, err := pipeline.GetElementByName("appsink") 38 - if err != nil { 39 - return err 40 - } 41 - 42 - go func() { 43 - HandleBusMessages(ctx, pipeline) 44 - cancel() 45 - }() 46 - 47 - sink := app.SinkFromElement(appsink) 48 - sink.SetCallbacks(&app.SinkCallbacks{ 49 - NewSampleFunc: WriterNewSample(ctx, w), 50 - EOSFunc: func(sink *app.Sink) { 51 - cancel() 52 - }, 53 - }) 54 - 55 - pipeline.SetState(gst.StatePlaying) 56 - 57 - <-ctx.Done() 58 - 59 - pipeline.BlockSetState(gst.StateNull) 60 - 61 - return nil 62 - }
-102
pkg/media/validate.go
··· 1 - package media 2 - 3 - import ( 4 - "bytes" 5 - "context" 6 - "fmt" 7 - "io" 8 - "strings" 9 - 10 - "stream.place/streamplace/pkg/constants" 11 - "stream.place/streamplace/pkg/crypto/signers" 12 - "stream.place/streamplace/pkg/log" 13 - "stream.place/streamplace/pkg/media/segchanman" 14 - "stream.place/streamplace/pkg/model" 15 - 16 - "git.stream.place/streamplace/c2pa-go/pkg/c2pa" 17 - ) 18 - 19 - func (mm *MediaManager) ValidateMP4(ctx context.Context, input io.Reader) error { 20 - buf, err := io.ReadAll(input) 21 - if err != nil { 22 - return err 23 - } 24 - r := bytes.NewReader(buf) 25 - reader, err := c2pa.FromStream(r, "video/mp4") 26 - if err != nil { 27 - return err 28 - } 29 - mani := reader.GetActiveManifest() 30 - certs := reader.GetProvenanceCertChain() 31 - pub, err := signers.ParseES256KCert([]byte(certs)) 32 - if err != nil { 33 - return err 34 - } 35 - meta, err := ParseSegmentAssertions(mani) 36 - if err != nil { 37 - return err 38 - } 39 - mediaData, err := mm.ParseSegmentMediaData(ctx, buf) 40 - if err != nil { 41 - return err 42 - } 43 - // special case for test signers that are only signed with a key 44 - var repoDID string 45 - var signingKeyDID string 46 - if strings.HasPrefix(meta.Creator, constants.DID_KEY_PREFIX) { 47 - signingKeyDID = meta.Creator 48 - repoDID = meta.Creator 49 - } else { 50 - repo, err := mm.atsync.SyncBlueskyRepoCached(ctx, meta.Creator, mm.model) 51 - if err != nil { 52 - return err 53 - } 54 - signingKey, err := mm.model.GetSigningKey(pub.DIDKey(), repo.DID) 55 - if err != nil { 56 - return err 57 - } 58 - if signingKey == nil { 59 - return fmt.Errorf("no signing key found for %s", pub.DIDKey()) 60 - } 61 - repoDID = repo.DID 62 - signingKeyDID = signingKey.DID 63 - } 64 - 65 - err = mm.cli.StreamIsAllowed(repoDID) 66 - if err != nil { 67 - return fmt.Errorf("got valid segment, but user %s is not allowed: %w", repoDID, err) 68 - } 69 - fd, err := mm.cli.SegmentFileCreate(repoDID, meta.StartTime, "mp4") 70 - if err != nil { 71 - return err 72 - } 73 - defer fd.Close() 74 - go mm.replicator.NewSegment(ctx, buf) 75 - r = bytes.NewReader(buf) 76 - io.Copy(fd, r) 77 - scmSeg := &segchanman.Seg{ 78 - Filepath: fd.Name(), 79 - Data: buf, 80 - } 81 - go mm.PublishSegment(ctx, repoDID, "source", scmSeg) 82 - seg := &model.Segment{ 83 - ID: *mani.Label, 84 - SigningKeyDID: signingKeyDID, 85 - RepoDID: repoDID, 86 - StartTime: meta.StartTime.Time(), 87 - Title: meta.Title, 88 - MediaData: mediaData, 89 - } 90 - mm.newSegmentSubsMutex.RLock() 91 - defer mm.newSegmentSubsMutex.RUnlock() 92 - not := &NewSegmentNotification{ 93 - Segment: seg, 94 - Data: buf, 95 - Metadata: meta, 96 - } 97 - for _, ch := range mm.newSegmentSubs { 98 - go func() { ch <- not }() 99 - } 100 - log.Log(ctx, "successfully ingested segment", "user", repoDID, "signingKey", signingKeyDID, "timestamp", meta.StartTime, "segmentID", *mani.Label) 101 - return nil 102 - }
+7 -5
pkg/media/webrtc.go
··· 24 24 var DEFAULT_DURATION = time.Duration(32 * time.Millisecond) 25 25 26 26 // This function remains in scope for the duration of a single users' playback 27 - func (mm *MediaManager) WebRTCPlayback(ctx context.Context, user string, rendition string, offer *webrtc.SessionDescription) (*webrtc.SessionDescription, error) { 27 + func (mm *MediaManager) WebRTCPlayback(ctx context.Context, user string, offer *webrtc.SessionDescription) (*webrtc.SessionDescription, error) { 28 28 uu, err := uuid.NewV7() 29 29 if err != nil { 30 30 return nil, err ··· 49 49 cancel() 50 50 }() 51 51 52 - outputQueue, done, err := ConcatStream(ctx, pipeline, user, rendition, mm) 52 + outputQueue, done, err := ConcatStream(ctx, pipeline, user, mm) 53 53 if err != nil { 54 54 return nil, fmt.Errorf("failed to get output queue: %w", err) 55 55 } ··· 305 305 }) 306 306 307 307 <-ctx.Done() 308 + log.Warn(ctx, "!!!!!!!!!!!!!!!!!!!!!!! ctx done") 308 309 }() 309 310 select { 310 311 case <-gatherComplete: ··· 390 391 pipelineSlice := []string{ 391 392 "multiqueue name=queue", 392 393 "appsrc format=time is-live=true do-timestamp=true name=videosrc ! capsfilter caps=application/x-rtp ! rtph264depay ! capsfilter caps=video/x-h264,stream-format=byte-stream,alignment=nal ! h264parse ! h264timestamper ! identity ! queue.sink_0", 393 - "appsrc format=time is-live=true do-timestamp=true name=audiosrc ! capsfilter caps=application/x-rtp,media=audio,encoding-name=OPUS,payload=111 ! rtpopusdepay ! opusdec use-inband-fec=true ! audiorate ! opusenc ! queue.sink_1", 394 + "appsrc format=time is-live=true do-timestamp=true name=audiosrc ! capsfilter caps=application/x-rtp,media=audio,encoding-name=OPUS,payload=111 ! rtpopusdepay ! queue.sink_1", 394 395 } 395 396 396 397 pipeline, err := gst.NewPipelineFromString(strings.Join(pipelineSlice, "\n")) ··· 524 525 peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { 525 526 log.Log(ctx, "Peer Connection State has changed", "state", s.String()) 526 527 527 - if s == webrtc.PeerConnectionStateFailed || s == webrtc.PeerConnectionStateDisconnected { 528 + if s == webrtc.PeerConnectionStateFailed { 528 529 // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. 529 530 // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. 530 531 // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. 531 - log.Log(ctx, "Peer Connection has ended, exiting", "state", s.String()) 532 + log.Log(ctx, "Peer Connection has gone to failed exiting") 532 533 cancel() 533 534 } 534 535 }) ··· 613 614 }) 614 615 615 616 <-ctx.Done() 617 + log.Warn(ctx, "!!!!!!!!! context done, exiting") 616 618 }() 617 619 select { 618 620 case <-gatherComplete:
+5 -13
pkg/model/segment.go
··· 13 13 ) 14 14 15 15 type SegmentMediadataVideo struct { 16 - Width int `json:"width"` 17 - Height int `json:"height"` 18 - FPSNum int `json:"fpsNum"` 19 - FPSDen int `json:"fpsDen"` 16 + Width int `json:"width"` 17 + Height int `json:"height"` 18 + Framerate string `json:"framerate"` 20 19 } 21 20 22 21 type SegmentMediadataAudio struct { ··· 25 24 } 26 25 27 26 type SegmentMediaData struct { 28 - Video []*SegmentMediadataVideo `json:"video"` 29 - Audio []*SegmentMediadataAudio `json:"audio"` 30 - Duration int64 `json:"duration"` 27 + Video []*SegmentMediadataVideo `json:"video"` 28 + Audio []*SegmentMediadataAudio `json:"audio"` 31 29 } 32 30 33 31 // Scan scan value into Jsonb, implements sql.Scanner interface ··· 70 68 if len(s.MediaData.Audio) == 0 || s.MediaData.Audio[0] == nil { 71 69 return nil, fmt.Errorf("audio data is nil") 72 70 } 73 - duration := s.MediaData.Duration 74 71 return &streamplace.Segment{ 75 72 LexiconTypeID: "place.stream.segment", 76 73 Creator: s.RepoDID, 77 74 Id: s.ID, 78 75 SigningKey: s.SigningKeyDID, 79 76 StartTime: string(aqt), 80 - Duration: &duration, 81 77 Video: []*streamplace.Segment_Video{ 82 78 { 83 79 Codec: "h264", 84 80 Width: int64(s.MediaData.Video[0].Width), 85 81 Height: int64(s.MediaData.Video[0].Height), 86 - Framerate: &streamplace.Segment_Framerate{ 87 - Num: int64(s.MediaData.Video[0].FPSNum), 88 - Den: int64(s.MediaData.Video[0].FPSDen), 89 - }, 90 82 }, 91 83 }, 92 84 Audio: []*streamplace.Segment_Audio{
-210
pkg/renditions/renditions.go
··· 1 - package renditions 2 - 3 - import ( 4 - "fmt" 5 - "math" 6 - 7 - "stream.place/streamplace/pkg/streamplace" 8 - ) 9 - 10 - type FPS struct { 11 - Passthrough bool 12 - Num uint 13 - Den uint 14 - } 15 - 16 - type Rendition struct { 17 - Width int64 18 - Height int64 19 - Bitrate int 20 - Framerate FPS 21 - Profile string 22 - Name string 23 - Parent *Rendition 24 - } 25 - 26 - type JsonProfile struct { 27 - Name string `json:"name,omitempty"` 28 - Width int `json:"width,omitempty"` 29 - Height int `json:"height,omitempty"` 30 - Bitrate int `json:"bitrate,omitempty"` 31 - FPS uint `json:"fps,omitempty"` 32 - FPSDen uint `json:"fpsDen,omitempty"` 33 - Profile string `json:"profile,omitempty"` 34 - GOP string `json:"gop,omitempty"` 35 - Encoder string `json:"encoder,omitempty"` 36 - Quality uint `json:"quality,omitempty"` 37 - } 38 - 39 - func (r Rendition) ToLivepeerProfile() JsonProfile { 40 - p := JsonProfile{ 41 - Name: r.Name, 42 - Bitrate: r.Bitrate, 43 - FPS: r.Framerate.Num, 44 - FPSDen: r.Framerate.Den, 45 - Profile: r.Profile, 46 - } 47 - if r.Parent == nil { 48 - p.Width = int(r.Width) 49 - p.Height = int(r.Height) 50 - } else { 51 - // We want to set the dimension that is the same as the parent 52 - if r.Width < r.Height { 53 - if r.Parent.Width == r.Height { 54 - p.Height = int(r.Parent.Width) 55 - } else { 56 - p.Width = int(r.Parent.Height) 57 - } 58 - } else { 59 - if r.Parent.Height == r.Height { 60 - p.Height = int(r.Parent.Height) 61 - } else { 62 - p.Width = int(r.Parent.Width) 63 - } 64 - } 65 - } 66 - return p 67 - } 68 - 69 - type Renditions []Rendition 70 - 71 - func (rs Renditions) ToLivepeerProfiles() []JsonProfile { 72 - profiles := make([]JsonProfile, len(rs)) 73 - for i, r := range rs { 74 - profiles[i] = r.ToLivepeerProfile() 75 - } 76 - return profiles 77 - } 78 - 79 - var DesiredRenditions = []Rendition{ 80 - { 81 - Name: "1080p", 82 - Width: 1920, 83 - Height: 1080, 84 - Bitrate: 6_000_000, 85 - Framerate: FPS{ 86 - Num: 60, 87 - Den: 1, 88 - }, 89 - Profile: "h264constrainedhigh", 90 - }, 91 - { 92 - Name: "720p", 93 - Width: 1280, 94 - Height: 720, 95 - Bitrate: 3_000_000, 96 - Framerate: FPS{ 97 - Num: 60, 98 - Den: 1, 99 - }, 100 - Profile: "h264constrainedhigh", 101 - }, 102 - { 103 - Name: "360p", 104 - Width: 640, 105 - Height: 360, 106 - Bitrate: 1_000_000, 107 - Framerate: FPS{ 108 - Num: 30, 109 - Den: 1, 110 - }, 111 - Profile: "h264constrainedhigh", 112 - }, 113 - { 114 - Name: "240p", 115 - Width: 426, 116 - Height: 240, 117 - Bitrate: 500_000, 118 - Framerate: FPS{ 119 - Num: 30, 120 - Den: 1, 121 - }, 122 - Profile: "h264constrainedhigh", 123 - }, 124 - { 125 - Name: "160p", 126 - Width: 284, 127 - Height: 160, 128 - Bitrate: 250_000, 129 - Framerate: FPS{ 130 - Num: 30, 131 - Den: 1, 132 - }, 133 - Profile: "h264baseline", 134 - }, 135 - } 136 - 137 - // GenerateRenditions generates renditions for a given spseg 138 - func GenerateRenditions(spseg *streamplace.Segment) (Renditions, error) { 139 - vid := spseg.Video[0] 140 - if vid == nil { 141 - return nil, fmt.Errorf("no video stream found") 142 - } 143 - rs := []Rendition{} 144 - for _, r := range DesiredRenditions { 145 - vidWidth := int64(vid.Width) 146 - vidHeight := int64(vid.Height) 147 - vertical := vid.Height > vid.Width 148 - // do all the math as if it's horizontal then flip at the end 149 - if vertical { 150 - vidWidth, vidHeight = vidHeight, vidWidth 151 - } 152 - if vidWidth <= r.Width && vidHeight <= r.Height { 153 - continue 154 - } 155 - rAspectRatio := float64(r.Width) / float64(r.Height) 156 - vidAspectRatio := float64(vidWidth) / float64(vidHeight) 157 - if vidAspectRatio > rAspectRatio { 158 - // vid is wider than r 159 - // scale down to r.Width 160 - scale := float64(r.Width) / float64(vidWidth) 161 - vidWidth = r.Width 162 - vidHeight = int64(math.Round(float64(vidHeight) * scale)) 163 - } else { 164 - // vid is taller than r 165 - // scale down to r.Height 166 - scale := float64(r.Height) / float64(vidHeight) 167 - vidHeight = r.Height 168 - vidWidth = int64(math.Round(float64(vidWidth) * scale)) 169 - } 170 - outR := Rendition{ 171 - Name: r.Name, 172 - Parent: &r, 173 - Profile: r.Profile, 174 - } 175 - if vertical { 176 - outR.Width = vidHeight 177 - outR.Height = vidWidth 178 - } else { 179 - outR.Width = vidWidth 180 - outR.Height = vidHeight 181 - } 182 - 183 - // if vertical { 184 - // ratio := float64(r.Height) / float64(vid.Height) 185 - // outR.Height = int64(float64(vid.Width) * (16.0 / 9.0) * ratio) 186 - // outR.Width = r.Height 187 - // } else { 188 - // ratio := float64(r.Width) / float64(vid.Width) 189 - // outR.Width = r.Width 190 - // outR.Height = int64(float64(vid.Width) * (9.0 / 16.0) * ratio) 191 - // } 192 - if vid.Framerate.Den > 0 { 193 - vidFPS := float64(vid.Framerate.Num) / float64(vid.Framerate.Den) 194 - rFPS := float64(r.Framerate.Num) / float64(r.Framerate.Den) 195 - delta := rFPS / vidFPS 196 - 197 - if rFPS < vidFPS { 198 - if delta < 0.75 { 199 - outR.Framerate.Num = uint(vid.Framerate.Num) 200 - outR.Framerate.Den = uint(vid.Framerate.Den * 2) 201 - } 202 - } 203 - } 204 - 205 - outR.Bitrate = r.Bitrate 206 - outR.Profile = r.Profile 207 - rs = append(rs, outR) 208 - } 209 - return rs, nil 210 - }
-323
pkg/renditions/renditions_test.go
··· 1 - package renditions 2 - 3 - import ( 4 - "encoding/json" 5 - "testing" 6 - 7 - "github.com/stretchr/testify/require" 8 - "stream.place/streamplace/pkg/streamplace" 9 - ) 10 - 11 - func seg(width int, height int, fpsNum int, fpsDen int) *streamplace.Segment { 12 - return &streamplace.Segment{ 13 - Video: []*streamplace.Segment_Video{ 14 - { 15 - Width: int64(width), 16 - Height: int64(height), 17 - Framerate: &streamplace.Segment_Framerate{ 18 - Num: int64(fpsNum), 19 - Den: int64(fpsDen), 20 - }, 21 - }, 22 - }, 23 - } 24 - } 25 - 26 - var cases = []struct { 27 - name string 28 - spseg *streamplace.Segment 29 - lp string 30 - }{ 31 - { 32 - name: "4K 60fps", 33 - spseg: seg(3840, 2160, 60, 1), 34 - lp: ` 35 - [ 36 - { 37 - "name": "1080p", 38 - "height": 1080, 39 - "bitrate": 6000000, 40 - "profile": "h264constrainedhigh" 41 - }, 42 - { 43 - "name": "720p", 44 - "height": 720, 45 - "bitrate": 3000000, 46 - "profile": "h264constrainedhigh" 47 - }, 48 - { 49 - "name": "360p", 50 - "height": 360, 51 - "bitrate": 1000000, 52 - "profile": "h264constrainedhigh", 53 - "fps": 60, 54 - "fpsDen": 2 55 - }, 56 - { 57 - "name": "240p", 58 - "height": 240, 59 - "bitrate": 500000, 60 - "profile": "h264constrainedhigh", 61 - "fps": 60, 62 - "fpsDen": 2 63 - }, 64 - { 65 - "name": "160p", 66 - "height": 160, 67 - "bitrate": 250000, 68 - "profile": "h264baseline", 69 - "fps": 60, 70 - "fpsDen": 2 71 - } 72 - ] 73 - `, 74 - }, 75 - { 76 - name: "2K with fractional framerate", 77 - spseg: seg(2160, 1440, 60000, 1001), 78 - lp: ` 79 - [ 80 - { 81 - "name": "1080p", 82 - "height": 1080, 83 - "bitrate": 6000000, 84 - "profile": "h264constrainedhigh" 85 - }, 86 - { 87 - "name": "720p", 88 - "height": 720, 89 - "bitrate": 3000000, 90 - "profile": "h264constrainedhigh" 91 - }, 92 - { 93 - "name": "360p", 94 - "height": 360, 95 - "bitrate": 1000000, 96 - "profile": "h264constrainedhigh", 97 - "fps": 60000, 98 - "fpsDen": 2002 99 - }, 100 - { 101 - "name": "240p", 102 - "height": 240, 103 - "bitrate": 500000, 104 - "profile": "h264constrainedhigh", 105 - "fps": 60000, 106 - "fpsDen": 2002 107 - }, 108 - { 109 - "name": "160p", 110 - "height": 160, 111 - "bitrate": 250000, 112 - "profile": "h264baseline", 113 - "fps": 60000, 114 - "fpsDen": 2002 115 - } 116 - ] 117 - `, 118 - }, 119 - { 120 - name: "720p 50fps", 121 - spseg: seg(1280, 720, 50, 1), 122 - lp: ` 123 - [ 124 - { 125 - "name": "360p", 126 - "height": 360, 127 - "bitrate": 1000000, 128 - "profile": "h264constrainedhigh", 129 - "fps": 50, 130 - "fpsDen": 2 131 - }, 132 - { 133 - "name": "240p", 134 - "height": 240, 135 - "bitrate": 500000, 136 - "profile": "h264constrainedhigh", 137 - "fps": 50, 138 - "fpsDen": 2 139 - }, 140 - { 141 - "name": "160p", 142 - "height": 160, 143 - "bitrate": 250000, 144 - "profile": "h264baseline", 145 - "fps": 50, 146 - "fpsDen": 2 147 - } 148 - ] 149 - `, 150 - }, 151 - { 152 - name: "720p 30fps", 153 - spseg: seg(1280, 720, 30, 1), 154 - lp: ` 155 - [ 156 - { 157 - "name": "360p", 158 - "height": 360, 159 - "bitrate": 1000000, 160 - "profile": "h264constrainedhigh" 161 - }, 162 - { 163 - "name": "240p", 164 - "height": 240, 165 - "bitrate": 500000, 166 - "profile": "h264constrainedhigh" 167 - }, 168 - { 169 - "name": "160p", 170 - "height": 160, 171 - "bitrate": 250000, 172 - "profile": "h264baseline" 173 - } 174 - ] 175 - `, 176 - }, 177 - { 178 - name: "720p 25fps", 179 - spseg: seg(1280, 720, 25, 1), 180 - lp: ` 181 - [ 182 - { 183 - "name": "360p", 184 - "height": 360, 185 - "bitrate": 1000000, 186 - "profile": "h264constrainedhigh" 187 - }, 188 - { 189 - "name": "240p", 190 - "height": 240, 191 - "bitrate": 500000, 192 - "profile": "h264constrainedhigh" 193 - }, 194 - { 195 - "name": "160p", 196 - "height": 160, 197 - "bitrate": 250000, 198 - "profile": "h264baseline" 199 - } 200 - ] 201 - `, 202 - }, 203 - { 204 - name: "Vertical video 60fps", 205 - spseg: seg(480, 640, 60, 1), 206 - lp: ` 207 - [ 208 - { 209 - "name": "360p", 210 - "width": 360, 211 - "bitrate": 1000000, 212 - "profile": "h264constrainedhigh", 213 - "fps": 60, 214 - "fpsDen": 2 215 - }, 216 - { 217 - "name": "240p", 218 - "width": 240, 219 - "bitrate": 500000, 220 - "profile": "h264constrainedhigh", 221 - "fps": 60, 222 - "fpsDen": 2 223 - }, 224 - { 225 - "name": "160p", 226 - "width": 160, 227 - "bitrate": 250000, 228 - "profile": "h264baseline", 229 - "fps": 60, 230 - "fpsDen": 2 231 - } 232 - ] 233 - `, 234 - }, 235 - } 236 - 237 - func TestRenditions(t *testing.T) { 238 - for _, c := range cases { 239 - t.Run(c.name, func(t *testing.T) { 240 - rends, err := GenerateRenditions(c.spseg) 241 - require.NoError(t, err) 242 - lp := rends.ToLivepeerProfiles() 243 - bs, err := json.Marshal(lp) 244 - require.NoError(t, err) 245 - require.JSONEq(t, c.lp, string(bs)) 246 - }) 247 - } 248 - } 249 - 250 - var singleCases = []struct { 251 - name string 252 - spseg *streamplace.Segment 253 - lp string 254 - dimensions []int 255 - }{ 256 - { 257 - name: "Nearly-Square Landscape 4K 60fps", 258 - spseg: seg(3840, 3830, 60, 1), 259 - dimensions: []int{1083, 1080}, 260 - lp: ` 261 - { 262 - "name": "1080p", 263 - "height": 1080, 264 - "bitrate": 6000000, 265 - "profile": "h264constrainedhigh" 266 - } 267 - `, 268 - }, 269 - { 270 - name: "Nearly-Square Portrait 4K 60fps", 271 - spseg: seg(3830, 3840, 60, 1), 272 - dimensions: []int{1080, 1083}, 273 - lp: ` 274 - { 275 - "name": "1080p", 276 - "width": 1080, 277 - "bitrate": 6000000, 278 - "profile": "h264constrainedhigh" 279 - } 280 - `, 281 - }, 282 - { 283 - name: "Stupidly-Wide Landscape 4K 60fps", 284 - spseg: seg(5000, 2160, 60, 1), 285 - dimensions: []int{1920, 829}, 286 - lp: ` 287 - { 288 - "name": "1080p", 289 - "width": 1920, 290 - "bitrate": 6000000, 291 - "profile": "h264constrainedhigh" 292 - } 293 - `, 294 - }, 295 - { 296 - name: "Stupidly-Tall Portrait 4K 60fps", 297 - spseg: seg(2160, 5000, 60, 1), 298 - dimensions: []int{829, 1920}, 299 - lp: ` 300 - { 301 - "name": "1080p", 302 - "height": 1920, 303 - "bitrate": 6000000, 304 - "profile": "h264constrainedhigh" 305 - } 306 - `, 307 - }, 308 - } 309 - 310 - func TestSingleRendition(t *testing.T) { 311 - for _, c := range singleCases { 312 - t.Run(c.name, func(t *testing.T) { 313 - rends, err := GenerateRenditions(c.spseg) 314 - require.NoError(t, err) 315 - first := rends[0] 316 - require.Equal(t, c.dimensions, []int{int(first.Width), int(first.Height)}) 317 - lp := rends.ToLivepeerProfiles() 318 - bs, err := json.Marshal(lp[0]) 319 - require.NoError(t, err) 320 - require.JSONEq(t, c.lp, string(bs)) 321 - }) 322 - } 323 - }
+3 -281
pkg/streamplace/cbor_gen.go
··· 456 456 } 457 457 458 458 cw := cbg.NewCborWriter(w) 459 - fieldCount := 8 459 + fieldCount := 7 460 460 461 461 if t.Audio == nil { 462 - fieldCount-- 463 - } 464 - 465 - if t.Duration == nil { 466 462 fieldCount-- 467 463 } 468 464 ··· 595 591 } 596 592 if _, err := cw.WriteString(string(t.Creator)); err != nil { 597 593 return err 598 - } 599 - 600 - // t.Duration (int64) (int64) 601 - if t.Duration != nil { 602 - 603 - if len("duration") > 1000000 { 604 - return xerrors.Errorf("Value in field \"duration\" was too long") 605 - } 606 - 607 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("duration"))); err != nil { 608 - return err 609 - } 610 - if _, err := cw.WriteString(string("duration")); err != nil { 611 - return err 612 - } 613 - 614 - if t.Duration == nil { 615 - if _, err := cw.Write(cbg.CborNull); err != nil { 616 - return err 617 - } 618 - } else { 619 - if *t.Duration >= 0 { 620 - if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.Duration)); err != nil { 621 - return err 622 - } 623 - } else { 624 - if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-*t.Duration-1)); err != nil { 625 - return err 626 - } 627 - } 628 - } 629 - 630 594 } 631 595 632 596 // t.StartTime (string) (string) ··· 849 813 850 814 t.Creator = string(sval) 851 815 } 852 - // t.Duration (int64) (int64) 853 - case "duration": 854 - { 855 - 856 - b, err := cr.ReadByte() 857 - if err != nil { 858 - return err 859 - } 860 - if b != cbg.CborNull[0] { 861 - if err := cr.UnreadByte(); err != nil { 862 - return err 863 - } 864 - maj, extra, err := cr.ReadHeader() 865 - if err != nil { 866 - return err 867 - } 868 - var extraI int64 869 - switch maj { 870 - case cbg.MajUnsignedInt: 871 - extraI = int64(extra) 872 - if extraI < 0 { 873 - return fmt.Errorf("int64 positive overflow") 874 - } 875 - case cbg.MajNegativeInt: 876 - extraI = int64(extra) 877 - if extraI < 0 { 878 - return fmt.Errorf("int64 negative overflow") 879 - } 880 - extraI = -1 - extraI 881 - default: 882 - return fmt.Errorf("wrong type for int64 field: %d", maj) 883 - } 884 - 885 - t.Duration = (*int64)(&extraI) 886 - } 887 - } 888 816 // t.StartTime (string) (string) 889 817 case "startTime": 890 818 ··· 1122 1050 } 1123 1051 1124 1052 cw := cbg.NewCborWriter(w) 1125 - fieldCount := 4 1126 1053 1127 - if t.Framerate == nil { 1128 - fieldCount-- 1129 - } 1130 - 1131 - if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 1054 + if _, err := cw.Write([]byte{163}); err != nil { 1132 1055 return err 1133 1056 } 1134 1057 ··· 1199 1122 } 1200 1123 } 1201 1124 1202 - // t.Framerate (streamplace.Segment_Framerate) (struct) 1203 - if t.Framerate != nil { 1204 - 1205 - if len("framerate") > 1000000 { 1206 - return xerrors.Errorf("Value in field \"framerate\" was too long") 1207 - } 1208 - 1209 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("framerate"))); err != nil { 1210 - return err 1211 - } 1212 - if _, err := cw.WriteString(string("framerate")); err != nil { 1213 - return err 1214 - } 1215 - 1216 - if err := t.Framerate.MarshalCBOR(cw); err != nil { 1217 - return err 1218 - } 1219 - } 1220 1125 return nil 1221 1126 } 1222 1127 ··· 1245 1150 1246 1151 n := extra 1247 1152 1248 - nameBuf := make([]byte, 9) 1153 + nameBuf := make([]byte, 6) 1249 1154 for i := uint64(0); i < n; i++ { 1250 1155 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 1251 1156 if err != nil { ··· 1323 1228 } 1324 1229 1325 1230 t.Height = int64(extraI) 1326 - } 1327 - // t.Framerate (streamplace.Segment_Framerate) (struct) 1328 - case "framerate": 1329 - 1330 - { 1331 - 1332 - b, err := cr.ReadByte() 1333 - if err != nil { 1334 - return err 1335 - } 1336 - if b != cbg.CborNull[0] { 1337 - if err := cr.UnreadByte(); err != nil { 1338 - return err 1339 - } 1340 - t.Framerate = new(Segment_Framerate) 1341 - if err := t.Framerate.UnmarshalCBOR(cr); err != nil { 1342 - return xerrors.Errorf("unmarshaling t.Framerate pointer: %w", err) 1343 - } 1344 - } 1345 - 1346 - } 1347 - 1348 - default: 1349 - // Field doesn't exist on this type, so ignore it 1350 - if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 1351 - return err 1352 - } 1353 - } 1354 - } 1355 - 1356 - return nil 1357 - } 1358 - func (t *Segment_Framerate) MarshalCBOR(w io.Writer) error { 1359 - if t == nil { 1360 - _, err := w.Write(cbg.CborNull) 1361 - return err 1362 - } 1363 - 1364 - cw := cbg.NewCborWriter(w) 1365 - 1366 - if _, err := cw.Write([]byte{162}); err != nil { 1367 - return err 1368 - } 1369 - 1370 - // t.Den (int64) (int64) 1371 - if len("den") > 1000000 { 1372 - return xerrors.Errorf("Value in field \"den\" was too long") 1373 - } 1374 - 1375 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("den"))); err != nil { 1376 - return err 1377 - } 1378 - if _, err := cw.WriteString(string("den")); err != nil { 1379 - return err 1380 - } 1381 - 1382 - if t.Den >= 0 { 1383 - if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Den)); err != nil { 1384 - return err 1385 - } 1386 - } else { 1387 - if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.Den-1)); err != nil { 1388 - return err 1389 - } 1390 - } 1391 - 1392 - // t.Num (int64) (int64) 1393 - if len("num") > 1000000 { 1394 - return xerrors.Errorf("Value in field \"num\" was too long") 1395 - } 1396 - 1397 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("num"))); err != nil { 1398 - return err 1399 - } 1400 - if _, err := cw.WriteString(string("num")); err != nil { 1401 - return err 1402 - } 1403 - 1404 - if t.Num >= 0 { 1405 - if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Num)); err != nil { 1406 - return err 1407 - } 1408 - } else { 1409 - if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.Num-1)); err != nil { 1410 - return err 1411 - } 1412 - } 1413 - 1414 - return nil 1415 - } 1416 - 1417 - func (t *Segment_Framerate) UnmarshalCBOR(r io.Reader) (err error) { 1418 - *t = Segment_Framerate{} 1419 - 1420 - cr := cbg.NewCborReader(r) 1421 - 1422 - maj, extra, err := cr.ReadHeader() 1423 - if err != nil { 1424 - return err 1425 - } 1426 - defer func() { 1427 - if err == io.EOF { 1428 - err = io.ErrUnexpectedEOF 1429 - } 1430 - }() 1431 - 1432 - if maj != cbg.MajMap { 1433 - return fmt.Errorf("cbor input should be of type map") 1434 - } 1435 - 1436 - if extra > cbg.MaxLength { 1437 - return fmt.Errorf("Segment_Framerate: map struct too large (%d)", extra) 1438 - } 1439 - 1440 - n := extra 1441 - 1442 - nameBuf := make([]byte, 3) 1443 - for i := uint64(0); i < n; i++ { 1444 - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 1445 - if err != nil { 1446 - return err 1447 - } 1448 - 1449 - if !ok { 1450 - // Field doesn't exist on this type, so ignore it 1451 - if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 1452 - return err 1453 - } 1454 - continue 1455 - } 1456 - 1457 - switch string(nameBuf[:nameLen]) { 1458 - // t.Den (int64) (int64) 1459 - case "den": 1460 - { 1461 - maj, extra, err := cr.ReadHeader() 1462 - if err != nil { 1463 - return err 1464 - } 1465 - var extraI int64 1466 - switch maj { 1467 - case cbg.MajUnsignedInt: 1468 - extraI = int64(extra) 1469 - if extraI < 0 { 1470 - return fmt.Errorf("int64 positive overflow") 1471 - } 1472 - case cbg.MajNegativeInt: 1473 - extraI = int64(extra) 1474 - if extraI < 0 { 1475 - return fmt.Errorf("int64 negative overflow") 1476 - } 1477 - extraI = -1 - extraI 1478 - default: 1479 - return fmt.Errorf("wrong type for int64 field: %d", maj) 1480 - } 1481 - 1482 - t.Den = int64(extraI) 1483 - } 1484 - // t.Num (int64) (int64) 1485 - case "num": 1486 - { 1487 - maj, extra, err := cr.ReadHeader() 1488 - if err != nil { 1489 - return err 1490 - } 1491 - var extraI int64 1492 - switch maj { 1493 - case cbg.MajUnsignedInt: 1494 - extraI = int64(extra) 1495 - if extraI < 0 { 1496 - return fmt.Errorf("int64 positive overflow") 1497 - } 1498 - case cbg.MajNegativeInt: 1499 - extraI = int64(extra) 1500 - if extraI < 0 { 1501 - return fmt.Errorf("int64 negative overflow") 1502 - } 1503 - extraI = -1 - extraI 1504 - default: 1505 - return fmt.Errorf("wrong type for int64 field: %d", maj) 1506 - } 1507 - 1508 - t.Num = int64(extraI) 1509 1231 } 1510 1232 1511 1233 default:
-16
pkg/streamplace/streamdefs.go
··· 19 19 Record *appbskytypes.GraphBlock `json:"record" cborgen:"record"` 20 20 Uri string `json:"uri" cborgen:"uri"` 21 21 } 22 - 23 - // Defs_Rendition is a "rendition" in the place.stream.defs schema. 24 - // 25 - // RECORDTYPE: Defs_Rendition 26 - type Defs_Rendition struct { 27 - LexiconTypeID string `json:"$type,const=place.stream.defs#rendition" cborgen:"$type,const=place.stream.defs#rendition"` 28 - Name string `json:"name" cborgen:"name"` 29 - } 30 - 31 - // Defs_Renditions is a "renditions" in the place.stream.defs schema. 32 - // 33 - // RECORDTYPE: Defs_Renditions 34 - type Defs_Renditions struct { 35 - LexiconTypeID string `json:"$type,const=place.stream.defs#renditions" cborgen:"$type,const=place.stream.defs#renditions"` 36 - Renditions []*Defs_Rendition `json:"renditions" cborgen:"renditions"` 37 - }
-16
pkg/streamplace/streamlivestream.go
··· 50 50 Livestream_LivestreamView *Livestream_LivestreamView 51 51 Livestream_ViewerCount *Livestream_ViewerCount 52 52 Defs_BlockView *Defs_BlockView 53 - Defs_Renditions *Defs_Renditions 54 - Defs_Rendition *Defs_Rendition 55 53 ChatDefs_MessageView *ChatDefs_MessageView 56 54 } 57 55 ··· 68 66 t.Defs_BlockView.LexiconTypeID = "place.stream.defs#blockView" 69 67 return json.Marshal(t.Defs_BlockView) 70 68 } 71 - if t.Defs_Renditions != nil { 72 - t.Defs_Renditions.LexiconTypeID = "place.stream.defs#renditions" 73 - return json.Marshal(t.Defs_Renditions) 74 - } 75 - if t.Defs_Rendition != nil { 76 - t.Defs_Rendition.LexiconTypeID = "place.stream.defs#rendition" 77 - return json.Marshal(t.Defs_Rendition) 78 - } 79 69 if t.ChatDefs_MessageView != nil { 80 70 t.ChatDefs_MessageView.LexiconTypeID = "place.stream.chat.defs#messageView" 81 71 return json.Marshal(t.ChatDefs_MessageView) ··· 98 88 case "place.stream.defs#blockView": 99 89 t.Defs_BlockView = new(Defs_BlockView) 100 90 return json.Unmarshal(b, t.Defs_BlockView) 101 - case "place.stream.defs#renditions": 102 - t.Defs_Renditions = new(Defs_Renditions) 103 - return json.Unmarshal(b, t.Defs_Renditions) 104 - case "place.stream.defs#rendition": 105 - t.Defs_Rendition = new(Defs_Rendition) 106 - return json.Unmarshal(b, t.Defs_Rendition) 107 91 case "place.stream.chat.defs#messageView": 108 92 t.ChatDefs_MessageView = new(ChatDefs_MessageView) 109 93 return json.Unmarshal(b, t.ChatDefs_MessageView)
+3 -12
pkg/streamplace/streamsegment.go
··· 16 16 LexiconTypeID string `json:"$type,const=place.stream.segment" cborgen:"$type,const=place.stream.segment"` 17 17 Audio []*Segment_Audio `json:"audio,omitempty" cborgen:"audio,omitempty"` 18 18 Creator string `json:"creator" cborgen:"creator"` 19 - // duration: The duration of the segment in nanoseconds 20 - Duration *int64 `json:"duration,omitempty" cborgen:"duration,omitempty"` 21 19 // id: Unique identifier for the segment 22 20 Id string `json:"id" cborgen:"id"` 23 21 // signingKey: The DID of the signing key used for this segment ··· 34 32 Rate int64 `json:"rate" cborgen:"rate"` 35 33 } 36 34 37 - // Segment_Framerate is a "framerate" in the place.stream.segment schema. 38 - type Segment_Framerate struct { 39 - Den int64 `json:"den" cborgen:"den"` 40 - Num int64 `json:"num" cborgen:"num"` 41 - } 42 - 43 35 // Segment_Video is a "video" in the place.stream.segment schema. 44 36 type Segment_Video struct { 45 - Codec string `json:"codec" cborgen:"codec"` 46 - Framerate *Segment_Framerate `json:"framerate,omitempty" cborgen:"framerate,omitempty"` 47 - Height int64 `json:"height" cborgen:"height"` 48 - Width int64 `json:"width" cborgen:"width"` 37 + Codec string `json:"codec" cborgen:"codec"` 38 + Height int64 `json:"height" cborgen:"height"` 39 + Width int64 `json:"width" cborgen:"width"` 49 40 }