diff --git a/packages/app/config/index.js b/packages/app/config/index.js index 32bfae3d..84877eac 100644 --- a/packages/app/config/index.js +++ b/packages/app/config/index.js @@ -12,11 +12,10 @@ export default { alt: "/logo_alt.svg", full: "/logo_full.svg", }, - api: { - address: defaultRemotesOrigins.http_api,//process.env.NODE_ENV !== "production" ? `http://${window.location.hostname}:3000` : defaultRemotesOrigins.http_api, - }, - ws: { - address: defaultRemotesOrigins.ws_api, //process.env.NODE_ENV !== "production" ? `ws://${window.location.hostname}:3001` : defaultRemotesOrigins.ws_api, + remotes: { + mainApi: defaultRemotesOrigins.main_api, //process.env.NODE_ENV !== "production" ? `http://${window.location.hostname}:3000` : defaultRemotesOrigins.http_api + streamingApi: defaultRemotesOrigins.streaming_api, //process.env.NODE_ENV !== "production" ? `ws://${window.location.hostname}:3001` : defaultRemotesOrigins.ws_api + websocketApi: defaultRemotesOrigins.ws_api, }, app: { title: packagejson.name, @@ -41,6 +40,6 @@ export default { name: "Español" } ], - defaultLocale: "es", + defaultLocale: "en", } } \ No newline at end of file diff --git a/packages/app/constants/defaultSettings.json b/packages/app/constants/defaultSettings.json index 5b8190b3..bdb121fd 100644 --- a/packages/app/constants/defaultSettings.json +++ b/packages/app/constants/defaultSettings.json @@ -1,4 +1,5 @@ { + "themeVariant": "light", "forceMobileMode": false, "notifications_sound": true, "notifications_vibrate": true, @@ -13,6 +14,7 @@ "main", "explore", "saved", - "marketplace" + "marketplace", + "streams", ] } \ No newline at end of file diff --git a/packages/app/constants/routes.json b/packages/app/constants/routes.json index 8232575e..00f8233e 100644 --- a/packages/app/constants/routes.json +++ b/packages/app/constants/routes.json @@ -18,5 +18,15 @@ "id": "marketplace", "title": "Marketplace", "icon": "Package" + }, + { + "id": "streams", + "title": "Streams", + "icon": "Tv" + }, + { + "id": "streaming_control", + "title": "Streaming Control", + "icon": "Video" } ] \ No newline at end of file diff --git a/packages/app/constants/settings/app.jsx b/packages/app/constants/settings/app.jsx index 9924605d..8945a54f 100644 --- a/packages/app/constants/settings/app.jsx +++ b/packages/app/constants/settings/app.jsx @@ -146,14 +146,17 @@ export default [ "experimental": true }, { + "experimental": true, "id": "darkMode", "storaged": true, "group": "aspect", "type": "Switch", "icon": "Moon", "title": "Dark mode", - "emitEvent": "darkMode", - "experimental": true + "emitEvent": "theme.applyVariant", + "emissionValueUpdate": (value) => { + return value ? "dark" : "light" + }, }, { "id": "primaryColor", diff --git a/packages/app/package.json b/packages/app/package.json index 2c689ccb..9413bab7 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -74,6 +74,7 @@ "react-router-config": "^5.1.1", "react-router-dom": "6.2.1", "react-virtualized": "^9.22.3", + "rxjs": "^7.5.5", "store": "^2.0.12", "styled-components": "^5.3.3", "vite-ssr": "0.15.0" diff --git a/packages/app/src/extensions/api.extension.js b/packages/app/src/extensions/api.extension.js index d1e993a9..4acbbc5c 100644 --- a/packages/app/src/extensions/api.extension.js +++ b/packages/app/src/extensions/api.extension.js @@ -87,8 +87,8 @@ export default class ApiExtension extends Extension { } return new Bridge({ - origin: config.api.address, - wsOrigin: config.ws.address, + origin: config.remotes.mainApi, + wsOrigin: config.remotes.websocketApi, wsOptions: { autoConnect: false, }, diff --git a/packages/app/src/extensions/settings.extension.js b/packages/app/src/extensions/settings.extension.js index 97595e5c..ec845ea6 100644 --- a/packages/app/src/extensions/settings.extension.js +++ b/packages/app/src/extensions/settings.extension.js @@ -1,6 +1,7 @@ import { Extension } from "evite" import store from "store" import defaultSettings from "schemas/defaultSettings.json" +import { Observable } from "rxjs" export default class SettingsExtension extends Extension { constructor(app, main) { @@ -52,6 +53,23 @@ export default class SettingsExtension extends Extension { return this.settings[key] } + withEvent = (listenEvent, defaultValue) => { + let value = defaultValue ?? this.settings[key] ?? false + + const observable = new Observable((subscriber) => { + subscriber.next(value) + + window.app.eventBus.on(listenEvent, (to) => { + value = to + subscriber.next(value) + }) + }) + + return observable.subscribe((value) => { + return value + }) + } + window = { "settings": this } diff --git a/packages/app/src/extensions/theme.extension.jsx b/packages/app/src/extensions/theme.extension.jsx index adab64ad..913dcce9 100644 --- a/packages/app/src/extensions/theme.extension.jsx +++ b/packages/app/src/extensions/theme.extension.jsx @@ -9,7 +9,6 @@ export default class ThemeExtension extends Extension { this.themeManifestStorageKey = "theme" this.modificationStorageKey = "themeModifications" - this.variantStorageKey = "themeVariation" this.theme = null @@ -19,12 +18,9 @@ export default class ThemeExtension extends Extension { initializers = [ async () => { - this.mainContext.eventBus.on("darkMode", (value) => { - if (value) { - this.applyVariant("dark") - } else { - this.applyVariant("light") - } + this.mainContext.eventBus.on("theme.applyVariant", (value) => { + this.applyVariant(value) + this.setVariant(value) }) this.mainContext.eventBus.on("modifyTheme", (value) => { this.update(value) @@ -95,11 +91,11 @@ export default class ThemeExtension extends Extension { } getStoragedVariant = () => { - return store.get(this.variantStorageKey) + return app.settings.get("themeVariant") } setVariant = (variationKey) => { - return store.set(this.variantStorageKey, variationKey) + return app.settings.set("themeVariant", variationKey) } setModifications = (modifications) => { @@ -142,7 +138,6 @@ export default class ThemeExtension extends Extension { if (values) { this.currentVariant = variant this.update(values) - this.setVariant(variant) } } diff --git a/packages/app/src/layout/header/index.less b/packages/app/src/layout/header/index.less index aa967eb6..733f105e 100644 --- a/packages/app/src/layout/header/index.less +++ b/packages/app/src/layout/header/index.less @@ -1,20 +1,20 @@ @import "theme/index.less"; .app_header { - user-select : none; + user-select: none; --webkit-user-select: none; - display : flex; + display: flex; flex-direction: row; - align-items : center; - z-index : 100; + align-items: center; + z-index: 100; - height : @app_header_height !important; + height: @app_header_height !important; padding: 10px; transition: all ease-in-out 150ms; - background : var(--background-color-primary) !important; + background: var(--background-color-primary) !important; background-color: var(--background-color-primary) !important; border-bottom: 1px var(--border-color) solid; @@ -25,7 +25,8 @@ &.hidden { opacity: 0; - height : 0 !important; + height: 0 !important; padding: 0 !important; + border: 0 !important; } } \ No newline at end of file diff --git a/packages/app/src/layout/index.jsx b/packages/app/src/layout/index.jsx index a3bd56ef..a83f7bac 100644 --- a/packages/app/src/layout/index.jsx +++ b/packages/app/src/layout/index.jsx @@ -12,7 +12,7 @@ const LayoutRenders = { mobile: (props) => { return <antd.Layout className={classnames("app_layout", ["mobile"])} style={{ height: "100%" }}> <antd.Layout className="content_layout"> - <antd.Layout.Content className="layout_page"> + <antd.Layout.Content className={classnames("layout_page", ...props.layoutPageModesClassnames ?? [])}> <div className={classnames("fade-transverse-active", { "fade-transverse-leave": props.isOnTransition })}> {props.children} </div> @@ -28,7 +28,7 @@ const LayoutRenders = { <Sidebar user={props.user} /> <antd.Layout className="content_layout"> <Header /> - <antd.Layout.Content className="layout_page"> + <antd.Layout.Content className={classnames("layout_page", ...props.layoutPageModesClassnames ?? [])}> <div className={classnames("fade-transverse-active", { "fade-transverse-leave": props.isOnTransition })}> {props.children} </div> @@ -43,6 +43,7 @@ export default class Layout extends React.Component { state = { layoutType: "default", isOnTransition: false, + compactMode: false, } setLayout = (layout) => { @@ -62,6 +63,11 @@ export default class Layout extends React.Component { window.app.eventBus.on("transitionDone", () => { this.setState({ isOnTransition: false }) }) + window.app.eventBus.on("toogleCompactMode", (to) => { + this.setState({ + compactMode: to ?? !this.state.compactMode, + }) + }) if (window.app.settings.get("forceMobileMode") || window.app.isAppCapacitor() || Math.min(window.screen.width, window.screen.height) < 768 || navigator.userAgent.indexOf("Mobi") > -1) { window.isMobile = true @@ -85,6 +91,9 @@ export default class Layout extends React.Component { const layoutComponentProps = { ...this.props, ...this.state, + layoutPageModesClassnames: [{ + ["noMargin"]: this.state.compactMode, + }] } if (LayoutRenders[this.state.layoutType]) { diff --git a/packages/app/src/pages/streaming_control/index.jsx b/packages/app/src/pages/streaming_control/index.jsx new file mode 100644 index 00000000..a0c72309 --- /dev/null +++ b/packages/app/src/pages/streaming_control/index.jsx @@ -0,0 +1,140 @@ +import React from "react" +import * as antd from "antd" +import { Icons } from "components/Icons" + +import "./index.less" + +const StreamingKeyView = (props) => { + const [streamingKeyVisibility, setStreamingKeyVisibility] = React.useState(false) + + const toogleVisibility = (to) => { + setStreamingKeyVisibility(to ?? !streamingKeyVisibility) + } + + return <div className="streamingKeyString"> + {streamingKeyVisibility ? + <> + <Icons.EyeOff onClick={() => toogleVisibility()} /> + <h4> + {props.streamingKey ?? "No streaming key available"} + </h4> + </> : + <> + <Icons.Eye onClick={() => toogleVisibility()} /> + Show key + </> + } + </div> +} + +export default (props) => { + const [isConnected, setIsConnected] = React.useState(false) + const [targetServer, setTargetServer] = React.useState("No available server") + + const [streamingKey, setStreamingKey] = React.useState(null) + const [serverTier, setServerTier] = React.useState(null) + + const checkStreamingKey = async () => { + const result = await app.request.get.streamingKey().catch((error) => { + console.error(error) + antd.message.error(error.message) + + return null + }) + + if (result) { + setStreamingKey(result.key) + } + } + + const checkTagetServer = async () => { + const result = await app.request.get.targetStreamingServer() + + if (result) { + const targetServer = `${result.protocol}://${result.address}:${result.port}/${result.space}` + setTargetServer(targetServer) + } + } + + const regenerateStreamingKey = async () => { + antd.Modal.confirm({ + title: "Regenerate streaming key", + content: "Are you sure you want to regenerate the streaming key? After this, all other generated keys will be deleted.", + onOk: async () => { + const result = await app.request.post.regenerateStreamingKey().catch((error) => { + console.error(error) + antd.message.error(error.message) + + return null + }) + + if (result) { + setStreamingKey(result.key) + } + } + }) + } + + React.useEffect(() => { + checkStreamingKey() + checkTagetServer() + // TODO: Use UserTier controller to check streaming service tier + // by now, we just use a fixed value + setServerTier("basic") + }, []) + + return <div className="streamingControlPanel"> + <div> + <h2><Icons.MdSettingsInputAntenna /> Connection Status</h2> + <div> + <antd.Tag + color={isConnected ? "Blue" : "Red"} + icon={isConnected ? <Icons.MdOutlineVideocam /> : <Icons.MdOutlineVideocamOff />} + > + {isConnected ? "Connected" : "Disconnected"} + </antd.Tag> + </div> + </div> + + <div> + <h2><Icons.Info /> Server info</h2> + <div className="info"> + <div className="label"> + <Icons.Server /> + Server Address + </div> + <div className="value"> + <h4> + {targetServer} + </h4> + </div> + </div> + <div className="info"> + <div className="label"> + <Icons.Key /> + Streaming Key + </div> + <div className="value"> + <StreamingKeyView streamingKey={streamingKey} /> + </div> + <div> + <antd.Button onClick={() => regenerateStreamingKey()}> + <Icons.RefreshCw /> + Regenerate + </antd.Button> + </div> + </div> + <div className="info"> + <div className="label"> + <Icons.MdSettingsInputSvideo /> + Usage Tier + </div> + <div className="value"> + <antd.Tag> + {serverTier} + </antd.Tag> + </div> + </div> + </div> + </div> +} \ No newline at end of file diff --git a/packages/app/src/pages/streaming_control/index.less b/packages/app/src/pages/streaming_control/index.less new file mode 100644 index 00000000..c128686c --- /dev/null +++ b/packages/app/src/pages/streaming_control/index.less @@ -0,0 +1,36 @@ +.streamingControlPanel { + display: inline-flex; + flex-direction: column; + + .info { + display: flex; + flex-direction: column; + + margin-bottom: 10px; + + .label { + + } + + .value { + margin-left: 10px; + font-family: "DM Mono", monospace; + + h4 { + // select all text + user-select: all; + margin: 0; + } + } + } + + > div { + margin-bottom: 20px; + } +} + +.streamingKeyString { + display: inline-flex; + flex-direction: row; + align-items: center; +} \ No newline at end of file diff --git a/packages/app/src/pages/streams/index.jsx b/packages/app/src/pages/streams/index.jsx index 59110796..1aafb96b 100644 --- a/packages/app/src/pages/streams/index.jsx +++ b/packages/app/src/pages/streams/index.jsx @@ -1,8 +1,9 @@ -import React from 'react' -import axios from "axios" +import React from "react" import * as antd from "antd" import { SelectableList, ActionsBar } from "components" +import "./index.less" + export default class Streams extends React.Component { state = { list: [], @@ -15,38 +16,69 @@ export default class Streams extends React.Component { } updateStreamsList = async () => { - const streams = await this.api.get.streams().catch(error => { + let streams = await this.api.get.streams().catch(error => { console.error(error) antd.message.error(error) - + return false }) + if (streams && Array.isArray(streams)) { + // resolve user_id with user basic data + streams = streams.map(async (stream) => { + const userData = await this.api.get.user(undefined, { user_id: stream.user_id }).catch((error) => { + console.error(error) + antd.message.error(error) + + return false + }) + + if (userData) { + stream.userData = userData + } + + return stream + }) + + streams = await Promise.all(streams) + } + this.setState({ list: streams }) } - onClickItem = (item) => { window.app.setLocation(`/streams/viewer?key=${item}`) } renderListItem = (stream) => { - stream.StreamPath = stream.StreamPath.replace(/^\/live\//, "") - - return <div key={stream.id} onClick={() => this.onClickItem(stream.StreamPath)}> - <h1>@{stream.StreamPath} #{stream.id}</h1> + return <div + key={stream.id} + onClick={() => this.onClickItem(stream.username)} + className="streaming-item" + > + <div className="thumbnail"> + <img src={stream.userData.avatar} alt={stream.userData.username} /> + </div> + <div className="details"> + <div className="title"> + <h1>@{stream.userData.username}</h1> + <span> + #{stream.id} + </span> + </div> + </div> </div> } render() { - return <div> + return <div className="streams"> <ActionsBar mode="float"> <div> <antd.Button onClick={this.updateStreamsList}>Refresh</antd.Button> </div> </ActionsBar> <div> - <SelectableList + <SelectableList selectionEnabled={false} renderItem={this.renderListItem} items={this.state.list} diff --git a/packages/app/src/pages/streams/index.less b/packages/app/src/pages/streams/index.less new file mode 100644 index 00000000..df0d631f --- /dev/null +++ b/packages/app/src/pages/streams/index.less @@ -0,0 +1,49 @@ +.streams { + .selectableList_content { + display: flex; + flex-wrap: wrap; + + .selectableList_item { + max-width: 20vw; + } + + .streaming-item { + display: flex; + flex-direction: column; + + align-items: center; + justify-content: center; + + padding: 10px; + + .thumbnail { + width: 15vw; + height: 100px; + background-size: cover; + + img { + width: 100%; + height: 100%; + } + } + + .details { + .title { + display: inline-flex; + align-items: center; + + h1{ + font-size: 1.5em; + font-weight: bold; + margin-right: 10px; + margin-bottom: 0; + } + + span { + font-size: 0.8em; + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/app/src/pages/streams/viewer/index.jsx b/packages/app/src/pages/streams/viewer/index.jsx index 815b3c27..56020326 100644 --- a/packages/app/src/pages/streams/viewer/index.jsx +++ b/packages/app/src/pages/streams/viewer/index.jsx @@ -1,15 +1,25 @@ -import React from 'react' +import React from "react" +import config from "config" import * as antd from "antd" -import Plyr from 'plyr' -import Hls from 'hls.js' -import mpegts from 'mpegts.js' +import { Icons } from "components/Icons" +import moment from "moment" + +import Plyr from "plyr" +import Hls from "hls.js" +import mpegts from "mpegts.js" import "plyr/dist/plyr.css" +import "./index.less" -const streamsSource = "http://media.ragestudio.net/live" +const streamsSource = config.remotes.streamingApi export default class StreamViewer extends React.Component { state = { + userData: null, + streamInfo: null, + spectators: 0, + timeFromNow: "00:00:00", + player: null, streamKey: null, streamSource: null, @@ -22,18 +32,98 @@ export default class StreamViewer extends React.Component { componentDidMount = async () => { const query = new URLSearchParams(window.location.search) - const requested = query.get("key") + const requestedUsername = query.get("key") - const source = `${streamsSource}/${requested}` - const player = new Plyr('#player') + const source = `${streamsSource}/${requestedUsername}` + const player = new Plyr("#player", { + autoplay: true, + controls: ["play", "mute", "volume", "fullscreen", "options", "settings"], + }) await this.setState({ player, - streamKey: requested, + streamKey: requestedUsername, streamSource: source, }) await this.loadWithProtocol[this.state.loadedProtocol]() + + // make the interface a bit confortable for a video player + app.ThemeController.applyVariant("dark") + app.eventBus.emit("toogleCompactMode", true) + app.SidebarController.toogleVisible(false) + app.HeaderController.toogleVisible(false) + + // fetch user info in the background + this.gatherUserInfo() + + // fetch stream info in the background + // await for it + await this.gatherStreamInfo() + + // create timer + if (this.state.streamInfo.connectCreated) { + this.createTimerCounter() + } + } + + componentWillUnmount = () => { + app.ThemeController.applyVariant(app.settings.get("themeVariant")) + app.eventBus.emit("toogleCompactMode", false) + app.SidebarController.toogleVisible(true) + app.HeaderController.toogleVisible(true) + app.HeaderController.toogleVisible(true) + + if (this.timerCounterInterval) { + this.timerCounterInterval = clearInterval(this.timerCounterInterval) + } + } + + gatherStreamInfo = async () => { + const result = await app.request.get.streamInfoFromUsername(undefined, { + username: this.state.streamKey, + }).catch((error) => { + console.error(error) + antd.message.error(error.message) + return false + }) + + if (result) { + this.setState({ + streamInfo: result, + }) + } + } + + gatherUserInfo = async () => { + const result = await app.request.get.user(undefined, { + username: this.state.streamKey, + }).catch((error) => { + console.error(error) + antd.message.error(error.message) + return false + }) + + if (result) { + this.setState({ + userData: result, + }) + } + } + + createTimerCounter = () => { + this.timerCounterInterval = setInterval(() => { + const secondsFromNow = moment().diff(moment(this.state.streamInfo.connectCreated), "seconds") + + // calculate hours minutes and seconds + const hours = Math.floor(secondsFromNow / 3600) + const minutes = Math.floor((secondsFromNow - hours * 3600) / 60) + const seconds = secondsFromNow - hours * 3600 - minutes * 60 + + this.setState({ + timeFromNow: `${hours}:${minutes}:${seconds}`, + }) + }, 1000) } updateQuality = (newQuality) => { @@ -60,10 +150,10 @@ export default class StreamViewer extends React.Component { console.log("Switching to " + protocol) this.loadWithProtocol[protocol]() } - + loadWithProtocol = { hls: () => { - const source = `${this.state.streamSource}.m3u8` + const source = `${streamsSource}/stream/hls/${this.state.streamKey}` const hls = new Hls() hls.loadSource(source) @@ -72,9 +162,13 @@ export default class StreamViewer extends React.Component { this.setState({ protocolInstance: hls, loadedProtocol: "hls" }) }, flv: () => { - const source = `${this.state.streamSource}.flv` + const source = `${streamsSource}/stream/flv/${this.state.streamKey}` - const instance = mpegts.createPlayer({ type: 'flv', url: source, isLive: true }) + const instance = mpegts.createPlayer({ + type: "flv", + url: source, + isLive: true + }) instance.attachMediaElement(this.videoPlayerRef.current) instance.load() @@ -85,15 +179,39 @@ export default class StreamViewer extends React.Component { } render() { - return <div> - <antd.Select - onChange={(value) => this.switchProtocol(value)} - value={this.state.loadedProtocol} - > - <antd.Select.Option value="hls">HLS</antd.Select.Option> - <antd.Select.Option value="flv">FLV</antd.Select.Option> - </antd.Select> + return <div className="stream"> <video ref={this.videoPlayerRef} id="player" /> + <div className="panel"> + <div className="info"> + <div className="title"> + <div> + <antd.Avatar + shape="square" + src={this.state.userData?.avatar} + /> + </div> + <div> + <h2>{this.state.userData?.username}</h2> + </div> + </div> + <div id="spectatorCount"> + <Icons.Eye /> + {this.state.spectators} + </div> + <div id="timeCount"> + <Icons.Clock /> + {this.state.timeFromNow} + </div> + </div> + <div className="chatbox"> + {/* TODO: Use chatbox component and join to stream channel using username */} + <antd.Result> + <h1> + Cannot connect with chat server + </h1> + </antd.Result> + </div> + </div> </div> } } \ No newline at end of file diff --git a/packages/app/src/pages/streams/viewer/index.less b/packages/app/src/pages/streams/viewer/index.less new file mode 100644 index 00000000..f9dbc8d5 --- /dev/null +++ b/packages/app/src/pages/streams/viewer/index.less @@ -0,0 +1,92 @@ +.plyr__controls { + width: 100%; + display: inline-flex; + //justify-content: space-between; +} + +.stream { + display: flex; + flex-direction: row; + + align-items: center; + justify-content: center; + + height: 100vh; + width: 100vw; + + color: var(--background-color-contrast); + + h1, + h2, + h3, + h4, + h5, + span, + p { + color: var(--background-color-contrast); + } + + .panel { + display: flex; + flex-direction: column; + + height: 100vh; + width: 20vw; + + .info { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + + width: 100%; + height: 10vh; + + padding: 10px; + + backdrop-filter: 20px; + + h1, + h2, + h3, + h4, + h5 { + margin: 0; + } + + >div { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + + height: fit-content; + margin-bottom: 8px; + + >div { + margin-right: 8px; + } + } + } + + .chatbox { + width: 20vw; + padding: 20px; + height: 100vh; + } + + #spectatorCount { + font-size: 0.8em; + } + + #timeCount { + font-size: 0.8em; + } + } + + .plyr { + border-radius: 0 4px 4px 0; + width: 80vw; + height: 100vh; + } +} \ No newline at end of file diff --git a/packages/app/src/theme/index.less b/packages/app/src/theme/index.less index 7a0b40b9..b5551b7f 100644 --- a/packages/app/src/theme/index.less +++ b/packages/app/src/theme/index.less @@ -5,29 +5,29 @@ ::-webkit-scrollbar { display: none; - width : 0; - height : 0; + width: 0; + height: 0; z-index: 0; } ::-webkit-scrollbar-thumb { position: absolute; - z-index : 200; + z-index: 200; - height : 6px; - margin : 5px 10px 5px 5px; + height: 6px; + margin: 5px 10px 5px 5px; transition: all 200ms ease-in-out; - border : 4px solid rgba(0, 0, 0, 0); + border: 4px solid rgba(0, 0, 0, 0); background-color: rgba(0, 0, 0, 0.15); - background-clip : padding-box; + background-clip: padding-box; -webkit-border-radius: 7px; } ::-webkit-scrollbar-button { - width : 0; - height : 0; + width: 0; + height: 0; display: none; } @@ -36,33 +36,33 @@ } html { - overflow : hidden; - height : 100%; + overflow: hidden; + height: 100%; -webkit-overflow-scrolling: touch; background-color: var(--background-color-primary) !important; svg { - margin-right : 10px; + margin-right: 10px; vertical-align: -0.125em; } } body { - overflow : hidden; + overflow: hidden; -webkit-overflow-scrolling: touch; - -webkit-app-region : no-drag; + -webkit-app-region: no-drag; height: 100%; - user-select : none; + user-select: none; --webkit-user-select: none; scroll-behavior: smooth; - text-rendering : optimizeLegibility !important; + text-rendering: optimizeLegibility !important; background-color: var(--background-color-primary) !important; - font-family : var(--fontFamily); + font-family: var(--fontFamily); } #root { @@ -71,7 +71,7 @@ body { position: fixed; overflow: hidden; - width : 100%; + width: 100%; height: 100%; background-color: var(--background-color-primary) !important; @@ -79,11 +79,11 @@ body { #nprogress { position: absolute; - top : 0; - width : 100vw; + top: 0; + width: 100vw; .bar { - height : 2px; + height: 2px; background: #48acf0; } } @@ -91,26 +91,26 @@ body { .ant-layout, .content_layout, .app_layout { - background : var(--background-color-primary) !important; + background: var(--background-color-primary) !important; background-color: var(--background-color-primary) !important; - position : relative; + position: relative; -webkit-overflow-scrolling: touch; - width : 100%; - height : 100%; + width: 100%; + height: 100%; max-height: 100vh; - overflow : hidden; + overflow: hidden; transition: all 150ms ease-in-out; ::-webkit-scrollbar { - display : block; + display: block; position: absolute; - width : 14px; - height : 18px; - z-index : 200; + width: 14px; + height: 18px; + z-index: 200; transition: all 200ms ease-in-out; } @@ -119,15 +119,15 @@ body { ::-webkit-scrollbar { display: none !important; - width : 0; - height : 0; + width: 0; + height: 0; z-index: 0; } } } .layout_page { - position : relative; + position: relative; -webkit-overflow-scrolling: touch; height: 100%; @@ -136,12 +136,18 @@ body { overflow-x: hidden; overflow-y: overlay; + + transition: all 150ms ease-in-out; + + &.noMargin { + margin: 0; + } } @media (max-width: 768px) { .layout_page { padding: 10px; - margin : 0; + margin: 0; } h1, @@ -152,7 +158,7 @@ body { h6, span, p { - user-select : none; + user-select: none; -webkit-user-select: none; } @@ -163,17 +169,17 @@ body { .fade-transverse-active { transition: all 250ms; - height : fit-content; - width : 100%; + height: fit-content; + width: 100%; } .fade-transverse-enter { - opacity : 0; + opacity: 0; transform: translateX(-30px); } .fade-transverse-leave { - opacity : 0; + opacity: 0; transform: translateX(30px); } @@ -183,18 +189,18 @@ body { } .fade-scale-enter { - opacity : 0; + opacity: 0; transform: scale(1.2); } .fade-scale-leave { - opacity : 0; + opacity: 0; transform: scale(0.8); } .fade-opacity-active { transition: all 250ms; - opacity : 1; + opacity: 1; } .fade-opacity-leave { @@ -206,50 +212,50 @@ body { } .app_initialization { - width : 100vw; - height : 100vh; + width: 100vw; + height: 100vh; padding: 50px; - display : flex; - flex-direction : column; + display: flex; + flex-direction: column; justify-content: center; - align-items : center; + align-items: center; >div { - width : 100%; + width: 100%; height: fit-content; - display : flex; - flex-direction : column; + display: flex; + flex-direction: column; justify-content: center; - align-items : center; + align-items: center; margin-bottom: 50px; } } .app_crash_wrapper { - width : 100vw; - height : 100vh; - display : flex; - flex-direction : column; + width: 100vw; + height: 100vh; + display: flex; + flex-direction: column; justify-content: center; - align-items : center; + align-items: center; } // Fixments .ant-btn { - display : flex; - align-items : center; + display: flex; + align-items: center; justify-content: center; - user-select : none; + user-select: none; --webkit-user-select: none; } .ant-result-extra { - display : flex; - align-items : center; + display: flex; + align-items: center; justify-content: center; } @@ -268,7 +274,7 @@ body { } *:not(input):not(textarea) { - -webkit-user-select : none; + -webkit-user-select: none; /* disable selection/Copy of UIWebView */ -webkit-touch-callout: none; /* disable the IOS popup when long-press on a link */ @@ -278,35 +284,35 @@ body { overflow: hidden; //background-color: rgba(240, 242, 245, 0.8); - backdrop-filter : blur(10px); + backdrop-filter: blur(10px); --webkit-backdrop-filter: blur(10px); - width : 100%; - height : 100%; + width: 100%; + height: 100%; z-index: 1000; - display : flex; + display: flex; flex-direction: column; - align-items : center; + align-items: center; justify-content: center; } .splash_logo { - width : 100%; + width: 100%; height: 100%; - display : flex; + display: flex; flex-direction: column; - align-items : center; + align-items: center; justify-content: center; img { - width : fit-content; - max-width : 50%; + width: fit-content; + max-width: 50%; max-height: 50%; - filter : drop-shadow(14px 10px 10px rgba(128, 128, 128, 0.5)); + filter: drop-shadow(14px 10px 10px rgba(128, 128, 128, 0.5)); } } @@ -318,4 +324,28 @@ body { to { opacity: 0; } +} + +.ant-result { + .ant-result-content { + display: inline-flex; + align-items: center; + justify-content: center; + + text-align: center; + + padding: 10px; + background-color: var(--background-color-accent); + color: var(--background-color-primary); + + h1, + h2, + h3, + h4, + h5, + p, + span { + margin: 0; + } + } } \ No newline at end of file diff --git a/packages/app/src/theme/variations/dark.less b/packages/app/src/theme/variations/dark.less index fc07b936..bcdd3baf 100644 --- a/packages/app/src/theme/variations/dark.less +++ b/packages/app/src/theme/variations/dark.less @@ -2,11 +2,17 @@ div { color: var(--text-color); } -h1, h2, h3, h4, h5, h6 { +h1, +h2, +h3, +h4, +h5, +h6 { color: var(--header-text-color); } -a, p { +a, +p { color: var(--text-color); } @@ -22,35 +28,40 @@ svg:not(.ant-tag *) { color: var(--svg-color); } -input, .ant-input-affix-wrapper, .ant-input { - color: var(--text-color)!important; +input, +.ant-input-affix-wrapper, +.ant-input { + color: var(--text-color) !important; background-color: var(--background-color-accent); } -input:disabled{ +input:disabled { background-color: var(--background_disabled); } // MODAL .ant-modal-content { - background-color: var(--background-color-accent)!important; + background-color: var(--background-color-accent) !important; } // TABLE tr { - background-color: var(--background-color-accent)!important; + background-color: var(--background-color-accent) !important; } -.ant-table, .ant-table-content, .ant-table-thead, .ant-table-cell { +.ant-table, +.ant-table-content, +.ant-table-thead, +.ant-table-cell { background-color: var(--background-color-accent); } -.ant-table-thead > tr > th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before { +.ant-table-thead>tr>th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before { background-color: var(--background-color-contrast); } // MENU -.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected { +.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected { background-color: var(--background-color-primary); } @@ -59,12 +70,12 @@ tr { background-color: var(--button-background-color); } -.ant-btn span{ - color: var(--button-text-color)!important; +.ant-btn span { + color: var(--button-text-color) !important; } .ant-btn svg { - color: var(--button-text-color)!important; + color: var(--button-text-color) !important; } // DRAWER diff --git a/packages/server/src/controllers/StreamingController/index.js b/packages/server/src/controllers/StreamingController/index.js new file mode 100644 index 00000000..9d94b51f --- /dev/null +++ b/packages/server/src/controllers/StreamingController/index.js @@ -0,0 +1,139 @@ +import { Controller } from "linebridge/dist/server" +import { User, StreamingKey } from "../../models" +import { nanoid } from "nanoid" + +import axios from "axios" + +const streamingMediaServer = process.env.streamingMediaServer ?? "media.ragestudio.net" +const streamingServerAPIAddress = process.env.streamingServerAPIAddress ?? "media.ragestudio.net" +const streamingServerAPIPort = process.env.streamingServerAPIPort ?? 3002 +const streamingServerAPIProtocol = process.env.streamingServerAPIProtocol ?? "http" +const streamingServerAPIUri = `${streamingServerAPIProtocol}://${streamingServerAPIAddress}:${streamingServerAPIPort}` + +export default class StreamingController extends Controller { + static useMiddlewares = ["withAuthentication"] + + methods = { + genereteKey: async (user_id) => { + // this will generate a new key for the user + // if the user already has a key, it will be regenerated + + // get username from user_id + const userData = await User.findOne({ user_id: user_id }) + + const streamingKey = new StreamingKey({ + user_id, + username: userData.username, + key: nanoid() + }) + + await streamingKey.save() + + return streamingKey + } + } + + get = { + "/stream_info_from_username": async (req, res) => { + const { username } = req.query + + const userspace = await StreamingKey.findOne({ username }) + + if (!userspace) { + return res.status(403).json({ + error: "This username has not a streaming key" + }) + } + + // TODO: meanwhile linebridge remote linkers are in development we gonna use this methods to fetch + const { data } = await axios.get(`${streamingServerAPIUri}/streams`, { + params: { + username: userspace.username + } + }).catch((error) => { + res.status(500).json({ + error: `Failed to fetch streams from [${streamingServerAPIAddress}]: ${error.message}` + }) + return false + }) + + return res.json(data) + }, + "/streams": async (req, res) => { + // TODO: meanwhile linebridge remote linkers are in development we gonna use this methods to fetch + const { data } = await axios.get(`${streamingServerAPIUri}/streams`).catch((error) => { + res.status(500).json({ + error: `Failed to fetch streams from [${streamingServerAPIAddress}]: ${error.message}` + }) + return false + }) + + if (data) { + return res.json(data) + } + }, + "/target_streaming_server": async (req, res) => { + // TODO: resolve an available server + // for now we just return the only one should be online + return res.json({ + protocol: "rtmp", + port: "1935", + space: "live", + address: streamingMediaServer, + }) + }, + "/streaming_key": async (req, res) => { + let streamingKey = await StreamingKey.findOne({ + user_id: req.user._id.toString() + }) + + if (!streamingKey) { + const newKey = await this.methods.genereteKey(req.user._id.toString()).catch(err => { + res.status(500).json({ + error: `Cannot generate a new key: ${err.message}`, + }) + + return false + }) + + if (!newKey) { + return false + } + + return res.json(newKey) + } else { + return res.json(streamingKey) + } + } + } + + post = { + "/regenerate_streaming_key": async (req, res) => { + // check if the user already has a key + let streamingKey = await StreamingKey.findOne({ + user_id: req.user._id.toString() + }) + + // if exists, delete it + + if (streamingKey) { + await streamingKey.remove() + } + + // generate a new key + const newKey = await this.methods.genereteKey(req.user._id.toString()).catch(err => { + res.status(500).json({ + error: `Cannot generate a new key: ${err.message}`, + }) + + return false + }) + + if (!newKey) { + return false + } + + return res.json(newKey) + } + } +} \ No newline at end of file diff --git a/packages/server/src/controllers/index.js b/packages/server/src/controllers/index.js index a19b7f97..adf6affe 100644 --- a/packages/server/src/controllers/index.js +++ b/packages/server/src/controllers/index.js @@ -5,6 +5,7 @@ import { default as UserController } from "./UserController" import { default as FilesController } from "./FilesController" import { default as PublicController } from "./PublicController" import { default as PostsController } from "./PostsController" +import { default as StreamingController } from "./StreamingController" export default [ PostsController, @@ -14,4 +15,5 @@ export default [ SessionController, UserController, FilesController, + StreamingController, ] \ No newline at end of file diff --git a/packages/server/src/models/index.js b/packages/server/src/models/index.js index 4d41330a..a416d2b3 100644 --- a/packages/server/src/models/index.js +++ b/packages/server/src/models/index.js @@ -28,5 +28,6 @@ export const Post = mongoose.model("Post", schemas.Post, "posts") export const Comment = mongoose.model("Comment", schemas.Comment, "comments") // streamings +export const StreamingKey = mongoose.model("StreamingKey", schemas.streamingKey, "streamingKeys") // marketplace \ No newline at end of file diff --git a/packages/server/src/schemas/index.js b/packages/server/src/schemas/index.js index 308fc09f..035ab475 100644 --- a/packages/server/src/schemas/index.js +++ b/packages/server/src/schemas/index.js @@ -5,4 +5,5 @@ export { default as Config } from "./config" export { default as Post } from "./post" export { default as Comment } from "./comment" export { default as UserFollow } from "./userFollow" -export { default as Badge } from "./badge" \ No newline at end of file +export { default as Badge } from "./badge" +export { default as streamingKey } from "./streamingKey" \ No newline at end of file diff --git a/packages/server/src/schemas/streamingKey/index.js b/packages/server/src/schemas/streamingKey/index.js new file mode 100644 index 00000000..ebb47e30 --- /dev/null +++ b/packages/server/src/schemas/streamingKey/index.js @@ -0,0 +1,14 @@ +export default { + username: { + type: String, + required: true, + }, + user_id: { + type: String, + required: true, + }, + key: { + type: String, + required: true, + } +} \ No newline at end of file diff --git a/packages/streaming-server/package.json b/packages/streaming-server/package.json new file mode 100644 index 00000000..8ceadf34 --- /dev/null +++ b/packages/streaming-server/package.json @@ -0,0 +1,20 @@ +{ + "name": "@comty/streaming-server", + "author": "RageStudio", + "version": "0.1.0", + "main": "dist/index.js", + "scripts": { + "dev": "nodemon --ignore dist/ --exec corenode-node ./src/index.js" + }, + "dependencies": { + "@ffmpeg-installer/ffmpeg": "^1.1.0", + "linebridge": "^0.11.13", + "lodash": "^4.17.21", + "mongoose": "^6.3.3", + "node-media-server": "^2.3.9" + }, + "devDependencies": { + "cross-env": "^7.0.3", + "nodemon": "^2.0.15" + } +} diff --git a/packages/streaming-server/src/index.js b/packages/streaming-server/src/index.js new file mode 100644 index 00000000..b7d2f0ea --- /dev/null +++ b/packages/streaming-server/src/index.js @@ -0,0 +1,307 @@ +const ffmpeg = require("@ffmpeg-installer/ffmpeg") +import lodash from "lodash" + +import { Server } from "linebridge/dist/server" +import MediaServer from "node-media-server" +import { SessionsManager, DbManager } from "./managers" +import { getStreamingKeyFromStreamPath } from "./lib" + +import axios from "axios" +import stream from "stream" + +import { StreamingKey } from "./models" + +const HTTPServerConfig = { + port: 3002, +} + +const MediaServerConfig = { + rtmp: { + port: 1935, + chunk_size: 60000, + gop_cache: true, + ping: 30, + ping_timeout: 60 + }, + http: { + port: 1000, + allow_origin: '*' + }, + trans: { + ffmpeg: ffmpeg.path, + tasks: [ + { + app: "live", + hls: true, + hlsFlags: "[hls_time=2:hls_list_size=3:hls_flags=delete_segments]", + } + ] + } +} + +const internalMediaServerURI = `http://127.0.0.1:${MediaServerConfig.http.port}` + +class StreamingServer { + IHTTPServer = new Server(HTTPServerConfig) + + IMediaServer = new MediaServer(MediaServerConfig) + + Db = new DbManager() + + Sessions = new SessionsManager() + + constructor() { + this.registerMediaServerEvents() + this.registerHTTPServerEndpoints() + + // fire initization + this.initialize() + } + + registerMediaServerEvents = () => { + Object.keys(this.mediaServerEvents).forEach((eventName) => { + this.IMediaServer.on(eventName, this.mediaServerEvents[eventName]) + }) + } + + registerHTTPServerEndpoints = () => { + Object.keys(this.httpServerEndpoints).forEach((route) => { + this.IHTTPServer.registerHTTPEndpoint({ + route: route, + ...this.httpServerEndpoints[route] + }) + }) + } + + httpServerEndpoints = { + "/events/on-publish": { + method: "post", + fn: async (req, res) => { + req.body = Buffer.from(req.body).toString() + + // decode url-encoded body + req.body = req.body.split("&").reduce((acc, cur) => { + const [key, value] = cur.split("=") + acc[key] = value + + return acc + }, {}) + + const streamingKey = req.body.name + + const streamingUserspace = await StreamingKey.findOne({ + key: streamingKey + }) + + if (!streamingUserspace) { + return res.status(403).send("Invalid stream key") + } + + this.Sessions.publishStream({ + user_id: streamingUserspace.user_id, + stream_key: streamingKey + }) + + return res.send("OK") + } + }, + "/events/on-publish-done": { + method: "post", + fn: async (req, res) => { + req.body = Buffer.from(req.body).toString() + + // decode url-encoded body + req.body = req.body.split("&").reduce((acc, cur) => { + const [key, value] = cur.split("=") + acc[key] = value + + return acc + }, {}) + + const streamingKey = req.body.name + + const streamingUserspace = await StreamingKey.findOne({ + key: streamingKey + }) + + if (!streamingUserspace) { + return res.status(403).send("Invalid stream key") + } + + this.Sessions.unpublishStream(streamingKey) + + return res.send("OK") + } + }, + "/streams": { + method: "get", + fn: async (req, res) => { + let streams = [] + + if (req.query?.username) { + streams = await this.Sessions.getStreamsByUsername(req.query?.username) + } else { + streams = this.Sessions.getPublicStreams() + } + + // retrieve streams details from internal media server api + let streamsListDetails = await axios.get(`${internalMediaServerURI}/api/streams`) + + streamsListDetails = streamsListDetails.data.live ?? {} + + // return only publisher details + streamsListDetails = Object.keys(streamsListDetails).map((streamKey) => { + return { + // filter unwanted properties + ...lodash.omit(streamsListDetails[streamKey].publisher, ["stream", "ip"]) + } + }) + + // reduce as an object + streamsListDetails = streamsListDetails.reduce((acc, cur) => { + acc[cur.clientId] = cur + + return acc + }, {}) + + // merge with public streams + streams = streams.map((stream) => { + return { + ...stream, + ...streamsListDetails[stream.id] + } + }) + + // if username is provided, return only streams for that user + // is supposed to be allowed only one stream per user + if (req.query?.username) { + return res.json(streams[0]) + } + + return res.json(streams) + } + }, + "/stream/:mode/:username": { + method: "get", + fn: async (req, res) => { + const { username, mode = "flv" } = req.params + + const streamSession = this.Sessions.publicStreams.find(stream => { + if (stream.username === username) { + return stream + } + }) + + if (!streamSession) { + return res.status(404).json({ + error: "Stream not found" + }) + } + + const streamKey = streamSession.stream_key + + switch (mode) { + case "flv": { + const streamingFLVUri = `${internalMediaServerURI}/live/${streamKey}.flv` + + // create a stream pipe response using media server api with axios + const request = await axios.get(streamingFLVUri, { + responseType: "stream" + }) + + // create a buffer stream from the request + const bufferStream = request.data.pipe(new stream.PassThrough()) + + // set header for stream response + res.setHeader("Content-Type", "video/x-flv") + + // pipe the buffer stream to the response + bufferStream.on("data", (chunk) => { + res.write(chunk) + }) + + break; + } + + case "hls": { + const streamingHLSUri = `${internalMediaServerURI}/live/${streamKey}.m3u8` + + // create a stream pipe response using media server api with axios + const request = await axios.get(streamingHLSUri, { + responseType: "stream" + }) + + // create a buffer stream from the request + const bufferStream = request.data.pipe(new stream.PassThrough()) + + // set header for stream response + res.setHeader("Content-Type", "application/x-mpegURL") + + // pipe the buffer stream to the response + bufferStream.on("data", (chunk) => { + res.write(chunk) + }) + + break; + } + + default: { + return res.status(400).json({ + error: "Stream mode not supported" + }) + } + } + } + } + } + + mediaServerEvents = { + prePublish: async (id, StreamPath, args) => { + // this event is fired before client is published + // here must be some validation (as key validation) + + // get session + const session = this.IMediaServer.getSession(id) + + // create a userspaced session for the client with containing session + this.Sessions.newSession(id, session) + + const streamingKey = getStreamingKeyFromStreamPath(StreamPath) + + const streamingUserspace = await StreamingKey.findOne({ + key: streamingKey + }) + + if (!streamingUserspace) { + this.Sessions.removeSession(id) + return false + } + + this.Sessions.publishStream({ + id, + user_id: streamingUserspace.user_id, + username: streamingUserspace.username, + stream_key: streamingKey + }) + }, + donePublish: async (id, StreamPath, args) => { + // this event is fired when client has ended the connection + + // stop the session + this.Sessions.removeSession(id) + + const streamingKey = getStreamingKeyFromStreamPath(StreamPath) + + this.Sessions.unpublishStream(streamingKey) + } + } + + initialize = async () => { + await this.Db.connect() + + await this.IHTTPServer.initialize() + await this.IMediaServer.run() + } +} + +new StreamingServer() \ No newline at end of file diff --git a/packages/streaming-server/src/lib/getStreamingKeyFromStreamPath/index.js b/packages/streaming-server/src/lib/getStreamingKeyFromStreamPath/index.js new file mode 100644 index 00000000..dd42ae54 --- /dev/null +++ b/packages/streaming-server/src/lib/getStreamingKeyFromStreamPath/index.js @@ -0,0 +1,3 @@ +export default function getStreamingKeyFromStreamPath(StreamPath) { + return StreamPath.split("/").pop() +} \ No newline at end of file diff --git a/packages/streaming-server/src/lib/index.js b/packages/streaming-server/src/lib/index.js new file mode 100644 index 00000000..a74fde00 --- /dev/null +++ b/packages/streaming-server/src/lib/index.js @@ -0,0 +1 @@ +export { default as getStreamingKeyFromStreamPath } from "./getStreamingKeyFromStreamPath" \ No newline at end of file diff --git a/packages/streaming-server/src/managers/DbManager/index.js b/packages/streaming-server/src/managers/DbManager/index.js new file mode 100644 index 00000000..3531eb2c --- /dev/null +++ b/packages/streaming-server/src/managers/DbManager/index.js @@ -0,0 +1,40 @@ +import mongoose from "mongoose" + +function parseConnectionString(obj) { + const { db_user, db_driver, db_name, db_pwd, db_hostname, db_port } = obj + return `${db_driver ?? "mongodb"}://${db_user ? `${db_user}` : ""}${db_pwd ? `:${db_pwd}` : ""}${db_user ? "@" : ""}${db_hostname ?? "localhost"}:${db_port ?? ""}/${db_name ?? ""}` +} + +export default class DBManager { + constructor() { + this.env = process.env + } + + connect = () => { + return new Promise((resolve, reject) => { + try { + console.log("🌐 Trying to connect to DB...") + const dbUri = parseConnectionString(this.env) + + //console.log(dbUri) + + mongoose.connect(dbUri, { + useNewUrlParser: true, + useUnifiedTopology: true + }) + .then((res) => { return resolve(true) }) + .catch((err) => { return reject(err) }) + } catch (err) { + return reject(err) + } + }).then(done => { + console.log(`✅ Connected to DB`) + }).catch((error) => { + console.log(`❌ Failed to connect to DB, retrying...\n`) + console.log(error) + setTimeout(() => { + this.connect() + }, 1000) + }) + } +} \ No newline at end of file diff --git a/packages/streaming-server/src/managers/SessionsManager/index.js b/packages/streaming-server/src/managers/SessionsManager/index.js new file mode 100644 index 00000000..def40749 --- /dev/null +++ b/packages/streaming-server/src/managers/SessionsManager/index.js @@ -0,0 +1,57 @@ +import lodash from "lodash" + +export default class SessionsManager { + constructor() { + this.sessions = {} + this.publicStreams = [] + } + + newSession = (id, session) => { + this.sessions[id] = session + } + + getSession = (id) => { + return this.sessions[id] + } + + removeSession = (id) => { + this.sessions[id].reject() + + delete this.sessions[id] + } + + publishStream = (payload) => { + if (typeof payload !== "object") { + throw new Error("Payload must be an object") + } + + this.publicStreams.push(payload) + } + + unpublishStream = (stream_key) => { + this.publicStreams = this.publicStreams.filter((stream) => stream.stream_key !== stream_key) + } + + getPublicStreams = () => { + // return this.publicStreams but without stream_key property + return lodash.map(this.publicStreams, (stream) => { + return lodash.omit(stream, "stream_key") + }) + } + + getStreamsByUserId = (user_id) => { + const streams = lodash.filter(this.publicStreams, (stream) => stream.user_id === user_id) + + return lodash.map(streams, (stream) => { + return lodash.omit(stream, "stream_key") + }) + } + + getStreamsByUsername = (username) => { + const streams = lodash.filter(this.publicStreams, (stream) => stream.username === username) + + return lodash.map(streams, (stream) => { + return lodash.omit(stream, "stream_key") + }) + } +} \ No newline at end of file diff --git a/packages/streaming-server/src/managers/index.js b/packages/streaming-server/src/managers/index.js new file mode 100644 index 00000000..11ce56ad --- /dev/null +++ b/packages/streaming-server/src/managers/index.js @@ -0,0 +1,2 @@ +export { default as DbManager } from "./DbManager" +export { default as SessionsManager } from "./SessionsManager" diff --git a/packages/streaming-server/src/models/index.js b/packages/streaming-server/src/models/index.js new file mode 100644 index 00000000..8945412e --- /dev/null +++ b/packages/streaming-server/src/models/index.js @@ -0,0 +1,18 @@ +import mongoose, { Schema } from "mongoose" + +function getSchemas() { + const obj = Object() + + const _schemas = require("../schemas") + + Object.keys(_schemas).forEach((key) => { + obj[key] = Schema(_schemas[key]) + }) + + return obj +} + +const schemas = getSchemas() + +// streaming +export const StreamingKey = mongoose.model("StreamingKey", schemas.streamingKey, "streamingKeys") \ No newline at end of file diff --git a/packages/streaming-server/src/schemas/StreamingKey/index.js b/packages/streaming-server/src/schemas/StreamingKey/index.js new file mode 100644 index 00000000..ebb47e30 --- /dev/null +++ b/packages/streaming-server/src/schemas/StreamingKey/index.js @@ -0,0 +1,14 @@ +export default { + username: { + type: String, + required: true, + }, + user_id: { + type: String, + required: true, + }, + key: { + type: String, + required: true, + } +} \ No newline at end of file diff --git a/packages/streaming-server/src/schemas/index.js b/packages/streaming-server/src/schemas/index.js new file mode 100644 index 00000000..1f405d09 --- /dev/null +++ b/packages/streaming-server/src/schemas/index.js @@ -0,0 +1 @@ +export { default as streamingKey } from "./streamingKey" \ No newline at end of file