mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
Merge pull request #133 from ragestudio/dev
This commit is contained in:
commit
81b0c72d1a
2
comty.js
2
comty.js
@ -1 +1 @@
|
||||
Subproject commit f408866ac27f7eb0ce2dd6abe1cfe4b1902cd7e8
|
||||
Subproject commit 94c8d7383e84a2de4b193d27adfcb1baa4163f68
|
@ -1 +1 @@
|
||||
Subproject commit bc9b82dab1767b2fa1085fcf22336c455a7f89c1
|
||||
Subproject commit fa61273d5b4b40a22d97c7773321d8ca6c985fd7
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comty/app",
|
||||
"version": "1.40.0@alpha",
|
||||
"version": "1.41.0@alpha",
|
||||
"license": "ComtyLicense",
|
||||
"main": "electron/main",
|
||||
"type": "module",
|
||||
@ -33,7 +33,7 @@
|
||||
"axios": "^1.7.7",
|
||||
"bear-react-carousel": "^4.0.10-alpha.0",
|
||||
"classnames": "2.3.1",
|
||||
"comty.js": "^0.65.0",
|
||||
"comty.js": "^0.65.5",
|
||||
"d3": "^7.9.0",
|
||||
"dashjs": "^5.0.0",
|
||||
"dompurify": "^3.0.0",
|
||||
|
@ -195,9 +195,11 @@ export default class ChunkedUpload {
|
||||
}
|
||||
|
||||
waitOnSSE(data) {
|
||||
console.log(`[UPLOADER] Connecting to SSE channel >`, data.sseUrl)
|
||||
// temporal solution until comty.js manages this
|
||||
const url = `${app.cores.api.client().mainOrigin}/upload/sse_events/${data.sseChannelId}`
|
||||
|
||||
const eventSource = new EventSource(data.sseUrl)
|
||||
console.log(`[UPLOADER] Connecting to SSE channel >`, url)
|
||||
const eventSource = new EventSource(url)
|
||||
|
||||
eventSource.onerror = (error) => {
|
||||
this.events.emit("error", error)
|
||||
|
@ -0,0 +1,44 @@
|
||||
import React from "react"
|
||||
import Hls from "hls.js"
|
||||
|
||||
const StreamPreview = ({ profile }) => {
|
||||
const videoRef = React.useRef(null)
|
||||
const hlsInstance = React.useRef(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
hlsInstance.current = new Hls({
|
||||
maxLiveSyncPlaybackRate: 1.5,
|
||||
strategy: "bandwidth",
|
||||
autoplay: true,
|
||||
})
|
||||
|
||||
hlsInstance.current.attachMedia(videoRef.current)
|
||||
|
||||
hlsInstance.current.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||
hlsInstance.current.loadSource(profile.sources.hls)
|
||||
})
|
||||
|
||||
videoRef.current.addEventListener("play", () => {
|
||||
console.log("[HLS] Syncing to last position")
|
||||
videoRef.current.currentTime = hlsInstance.current.liveSyncPosition
|
||||
})
|
||||
|
||||
videoRef.current.play()
|
||||
|
||||
return () => {
|
||||
hlsInstance.current.destroy()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<video
|
||||
muted
|
||||
autoplay
|
||||
controls
|
||||
ref={videoRef}
|
||||
id="stream_preview_player"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default StreamPreview
|
@ -9,7 +9,11 @@ const ProfileHeader = ({ profile, streamHealth }) => {
|
||||
|
||||
async function setTimedThumbnail() {
|
||||
setThumbnail(() => {
|
||||
if (streamRef.current.online && profile.info.thumbnail) {
|
||||
if (
|
||||
streamRef.current &&
|
||||
streamRef.current.online &&
|
||||
profile.info.thumbnail
|
||||
) {
|
||||
return `${profile.info.thumbnail}?t=${Date.now()}`
|
||||
}
|
||||
|
||||
|
@ -59,6 +59,10 @@ const ProfileData = (props) => {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!ws.state.connected) {
|
||||
return false
|
||||
}
|
||||
|
||||
const health = await ws.call("stream:health", profile_id)
|
||||
|
||||
setStreamHealth(health)
|
||||
|
@ -60,6 +60,7 @@
|
||||
top: 0;
|
||||
|
||||
z-index: 10;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.profile-header__content {
|
||||
@ -217,9 +218,6 @@
|
||||
background-color: var(--background-color-accent);
|
||||
|
||||
.ant-segmented-thumb {
|
||||
left: var(--thumb-active-left);
|
||||
width: var(--thumb-active-width);
|
||||
|
||||
background-color: var(--background-color-primary-2);
|
||||
|
||||
transition: all 150ms ease-in-out;
|
||||
|
@ -4,6 +4,7 @@ import UploadButton from "@components/UploadButton"
|
||||
import { FiImage, FiInfo } from "react-icons/fi"
|
||||
import { MdTextFields, MdDescription } from "react-icons/md"
|
||||
|
||||
import StreamPreview from "../../components/StreamPreview"
|
||||
import StreamRateChart from "../../components/StreamRateChart"
|
||||
import { formatBytes, formatBitrate } from "../../liveTabUtils"
|
||||
import { useStreamSignalQuality } from "../../useStreamSignalQuality"
|
||||
@ -146,7 +147,7 @@ const Live = ({ profile, loading, handleProfileUpdate, streamHealth }) => {
|
||||
</div>
|
||||
<div className="content-panel__content">
|
||||
<div className="status-indicator">
|
||||
Stream Status:{" "}
|
||||
Stream Status:
|
||||
{streamHealth?.online ? (
|
||||
<Tag color="green">Online</Tag>
|
||||
) : (
|
||||
@ -155,9 +156,14 @@ const Live = ({ profile, loading, handleProfileUpdate, streamHealth }) => {
|
||||
</div>
|
||||
|
||||
<div className="live-tab-preview">
|
||||
{streamHealth?.online
|
||||
? "Video Preview Area"
|
||||
: "Stream is Offline"}
|
||||
{streamHealth?.online ? (
|
||||
<StreamPreview
|
||||
streamHealth={streamHealth}
|
||||
profile={profile}
|
||||
/>
|
||||
) : (
|
||||
"Stream is Offline"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -27,7 +27,10 @@
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
height: 250px;
|
||||
|
||||
max-height: 250px;
|
||||
min-height: 250px;
|
||||
|
||||
background-color: var(--background-color-primary);
|
||||
|
||||
@ -35,6 +38,14 @@
|
||||
border-radius: 4px;
|
||||
|
||||
font-size: 1rem;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.live-tab-stats {
|
||||
|
@ -7,22 +7,19 @@ import "./index.less"
|
||||
|
||||
const ProfileCreator = (props) => {
|
||||
const [loading, setLoading] = React.useState(false)
|
||||
const [name, setName] = React.useState(props.editValue ?? null)
|
||||
const [title, setTitle] = React.useState(props.editValue ?? null)
|
||||
|
||||
function handleChange(e) {
|
||||
setName(e.target.value.trim())
|
||||
setTitle(e.target.value.trim())
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
setLoading(true)
|
||||
|
||||
if (props.editValue) {
|
||||
if (typeof props.onEdit === "function") {
|
||||
await props.onEdit(name)
|
||||
}
|
||||
} else {
|
||||
const result = await Streaming.createProfile({
|
||||
profile_name: name,
|
||||
info: {
|
||||
title: title,
|
||||
},
|
||||
}).catch((error) => {
|
||||
console.error(error)
|
||||
app.message.error("Failed to create")
|
||||
@ -32,9 +29,9 @@ const ProfileCreator = (props) => {
|
||||
if (result) {
|
||||
app.message.success("Created")
|
||||
app.eventBus.emit("app:new_profile", result)
|
||||
|
||||
props.onCreate(result._id, result)
|
||||
}
|
||||
}
|
||||
|
||||
props.close()
|
||||
|
||||
@ -44,8 +41,8 @@ const ProfileCreator = (props) => {
|
||||
return (
|
||||
<div className="profile-creator">
|
||||
<antd.Input
|
||||
value={name}
|
||||
placeholder="Enter a profile name"
|
||||
value={title}
|
||||
placeholder="Enter a profile title"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
|
||||
@ -55,12 +52,12 @@ const ProfileCreator = (props) => {
|
||||
<antd.Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
handleSubmit(name)
|
||||
handleSubmit(title)
|
||||
}}
|
||||
disabled={!name || loading}
|
||||
disabled={!title || loading}
|
||||
loading={loading}
|
||||
>
|
||||
{props.editValue ? "Update" : "Create"}
|
||||
Create
|
||||
</antd.Button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,56 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const Profile = ({
|
||||
profile,
|
||||
onClickManage,
|
||||
onClickChangeId,
|
||||
onClickDelete,
|
||||
}) => {
|
||||
if (!profile) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tvstudio-page-list-item">
|
||||
<div className="tvstudio-page-list-item__id">
|
||||
<antd.Tag>{profile._id}</antd.Tag>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="tvstudio-page-list-item__thumbnail"
|
||||
style={{
|
||||
backgroundImage: `url("${profile.info.offline_thumbnail}")`,
|
||||
}}
|
||||
onClick={onClickManage}
|
||||
/>
|
||||
|
||||
<div className="tvstudio-page-list-item__content">
|
||||
<div className="tvstudio-page-list-item__content__title">
|
||||
<h1>{profile.info.title}</h1>
|
||||
</div>
|
||||
|
||||
<div className="tvstudio-page-list-item__content__description">
|
||||
<span>{profile.info.description ?? "No description"}</span>
|
||||
</div>
|
||||
|
||||
<div className="tvstudio-page-list-item__content__actions">
|
||||
<antd.Button size="small" onClick={onClickManage}>
|
||||
Manage
|
||||
</antd.Button>
|
||||
<antd.Button
|
||||
size="small"
|
||||
type="danger"
|
||||
onClick={onClickDelete}
|
||||
>
|
||||
Delete
|
||||
</antd.Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Profile
|
@ -0,0 +1,107 @@
|
||||
.tvstudio-page-list-item {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
height: 130px;
|
||||
|
||||
background-color: var(--background-color-accent);
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 3px solid var(--border-color);
|
||||
}
|
||||
|
||||
&__id {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
&__thumbnail {
|
||||
position: absolute;
|
||||
z-index: 55;
|
||||
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
|
||||
opacity: 0.5;
|
||||
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
rgba(0, 0, 0, 0),
|
||||
rgba(0, 0, 0, 1)
|
||||
);
|
||||
}
|
||||
|
||||
&__content {
|
||||
z-index: 100;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 60%;
|
||||
height: 100%;
|
||||
|
||||
padding: 20px 20px;
|
||||
|
||||
gap: 5px;
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
&__description {
|
||||
display: flex;
|
||||
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
position: absolute;
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
margin: 10px 20px;
|
||||
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
.ant-btn {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +1,16 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
|
||||
import ProfileCreator from "./components/ProfileCreator"
|
||||
import Skeleton from "@components/Skeleton"
|
||||
|
||||
import ProfileCreator from "./components/ProfileCreator"
|
||||
import ProfileItem from "./components/ProfileItem"
|
||||
|
||||
import Streaming from "@models/spectrum"
|
||||
|
||||
import useCenteredContainer from "@hooks/useCenteredContainer"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const Profile = ({ profile, onClick }) => {
|
||||
return <div onClick={onClick}>{profile.profile_name}</div>
|
||||
}
|
||||
|
||||
const TVStudioPage = (props) => {
|
||||
useCenteredContainer(false)
|
||||
|
||||
const [loading, list, error, repeat] = app.cores.api.useRequest(
|
||||
Streaming.getOwnProfiles,
|
||||
)
|
||||
@ -25,39 +19,75 @@ const TVStudioPage = (props) => {
|
||||
app.layout.modal.open("tv_profile_creator", ProfileCreator, {
|
||||
props: {
|
||||
onCreate: (id, data) => {
|
||||
setSelectedProfileId(id)
|
||||
repeat()
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleProfileClick(id) {
|
||||
function handleDeleteProfileClick(id) {
|
||||
app.layout.modal.confirm({
|
||||
headerText: "Delete profile",
|
||||
descriptionText: "Are you sure you want to delete profile?",
|
||||
onConfirm: async () => {
|
||||
const result = await Streaming.deleteProfile(id)
|
||||
|
||||
if (result) {
|
||||
app.message.success("Profile deleted")
|
||||
repeat()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleManageProfileClick(id) {
|
||||
app.location.push(`/studio/tv/${id}`)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<antd.Result
|
||||
status="warning"
|
||||
title="Error"
|
||||
subTitle="Failed to fetch profiles"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Skeleton />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tvstudio-page">
|
||||
<div className="tvstudio-page-header">
|
||||
<h1>TV Studio</h1>
|
||||
</div>
|
||||
|
||||
<div className="tvstudio-page-actions">
|
||||
<antd.Button type="primary" onClick={handleNewProfileClick}>
|
||||
Create new
|
||||
</antd.Button>
|
||||
</div>
|
||||
|
||||
<div className="tvstudio-page-list">
|
||||
{list.length > 0 &&
|
||||
list.map((profile, index) => {
|
||||
return (
|
||||
<Profile
|
||||
<ProfileItem
|
||||
key={index}
|
||||
profile={profile}
|
||||
onClick={() => handleProfileClick(profile._id)}
|
||||
onClickManage={() =>
|
||||
handleManageProfileClick(profile._id)
|
||||
}
|
||||
onClickDelete={() =>
|
||||
handleDeleteProfileClick(profile._id)
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,26 @@
|
||||
|
||||
gap: 10px;
|
||||
|
||||
.tvstudio-page-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
width: fit-content;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
background-color: var(--background-color-accent);
|
||||
padding: 10px 20px;
|
||||
|
||||
border-radius: 12px;
|
||||
|
||||
h1 {
|
||||
font-family: "Space Grotesk", sans-serif;
|
||||
font-size: 1.3rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tvstudio-page-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -24,14 +44,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.tvstudio-page-selector-hint {
|
||||
.tvstudio-page-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
|
||||
padding: 50px 0;
|
||||
border-radius: 12px;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
7
packages/server/extra-proxies.js
Normal file
7
packages/server/extra-proxies.js
Normal file
@ -0,0 +1,7 @@
|
||||
export default {
|
||||
"/spectrum/*": {
|
||||
target: process.env.SPECTRUM_API ?? "https://live.ragestudio.net",
|
||||
pathRewrite: { "^/spectrum/(.*)": "/$1", "^/spectrum": "/" },
|
||||
websocket: true,
|
||||
},
|
||||
}
|
@ -165,6 +165,92 @@ export default class Gateway {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and registers additional proxy routes from ../../extra-proxies.js
|
||||
*/
|
||||
async registerExtraProxies() {
|
||||
try {
|
||||
// Dynamic import is relative to the current file.
|
||||
// extra-proxies.js can be CJS (module.exports = ...) or ESM (export default ...)
|
||||
const extraProxiesModule = require(
|
||||
path.resolve(process.cwd(), "extra-proxies.js"),
|
||||
)
|
||||
const extraProxies = extraProxiesModule.default // Node's CJS/ESM interop puts module.exports on .default
|
||||
|
||||
if (
|
||||
!extraProxies ||
|
||||
typeof extraProxies !== "object" ||
|
||||
Object.keys(extraProxies).length === 0
|
||||
) {
|
||||
console.log(
|
||||
"[Gateway] No extra proxies defined in `extra-proxies.js`, file is empty, or format is invalid. Skipping.",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[Gateway] Registering extra proxies from 'extra-proxies.js'...`,
|
||||
)
|
||||
|
||||
for (const proxyPathKey in extraProxies) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
extraProxies,
|
||||
proxyPathKey,
|
||||
)
|
||||
) {
|
||||
const config = extraProxies[proxyPathKey]
|
||||
if (!config || typeof config.target !== "string") {
|
||||
console.warn(
|
||||
`[Gateway] Skipping invalid extra proxy config for path: '${proxyPathKey}' in 'extra-proxies.js'. Target is missing or not a string.`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
let registrationPath = proxyPathKey
|
||||
|
||||
// Normalize paths ending with /*
|
||||
// e.g., "/spectrum/*" becomes "/spectrum"
|
||||
// e.g., "/*" becomes "/"
|
||||
if (registrationPath.endsWith("/*")) {
|
||||
registrationPath = registrationPath.slice(0, -2)
|
||||
if (registrationPath === "") {
|
||||
registrationPath = "/"
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[Gateway] Registering extra proxy: '${proxyPathKey}' (as '${registrationPath}') -> ${config.target}`,
|
||||
)
|
||||
|
||||
await this.gateway.register({
|
||||
serviceId: `extra-proxy:${registrationPath}`, // Unique ID for this proxy rule
|
||||
path: registrationPath,
|
||||
target: config.target,
|
||||
pathRewrite: config.pathRewrite, // undefined if not present
|
||||
websocket: !!config.websocket, // false if not present or falsy
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle cases where the extra-proxies.js file might not exist
|
||||
if (
|
||||
error.code === "ERR_MODULE_NOT_FOUND" ||
|
||||
(error.message &&
|
||||
error.message.toLowerCase().includes("cannot find module"))
|
||||
) {
|
||||
console.log(
|
||||
"[Gateway] `extra-proxies.js` not found. Skipping extra proxy registration.",
|
||||
)
|
||||
} else {
|
||||
console.error(
|
||||
"[Gateway] Error loading or registering extra proxies from `extra-proxies.js`:",
|
||||
error,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle both router and websocket registration requests from services
|
||||
* @param {Service} service - Service registering a route or websocket
|
||||
@ -304,6 +390,9 @@ export default class Gateway {
|
||||
await this.gateway.initialize()
|
||||
}
|
||||
|
||||
// Register any externally defined proxies before services start
|
||||
await this.registerExtraProxies()
|
||||
|
||||
// Watch for service state changes
|
||||
Observable.observe(this.serviceRegistry, (changes) => {
|
||||
this.checkAllServicesReady()
|
||||
|
@ -371,19 +371,12 @@ http {
|
||||
|
||||
if (route.pathRewrite && Object.keys(route.pathRewrite).length > 0) {
|
||||
rewriteConfig += "# Path rewrite rules\n"
|
||||
|
||||
for (const [pattern, replacement] of Object.entries(
|
||||
route.pathRewrite,
|
||||
)) {
|
||||
// Improved rewrite pattern that preserves query parameters
|
||||
rewriteConfig += `\trewrite ${pattern} ${replacement}$is_args$args break;`
|
||||
}
|
||||
} else {
|
||||
// If no explicit rewrite is defined, but we need to strip the path prefix,
|
||||
// Generate a default rewrite that preserves the URL structure
|
||||
if (path !== "/") {
|
||||
rewriteConfig += "# Default path rewrite to strip prefix\n"
|
||||
rewriteConfig += `\trewrite ^${path}(/.*)$ $1$is_args$args break;\n`
|
||||
rewriteConfig += `\trewrite ^${path}$ / break;`
|
||||
rewriteConfig += `\nrewrite ${pattern} ${replacement} break;`
|
||||
}
|
||||
}
|
||||
|
||||
@ -423,6 +416,8 @@ ${locationDirective} {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
${rewriteConfig}
|
||||
|
||||
# Proxy pass to service
|
||||
proxy_pass ${route.target};
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ export default {
|
||||
return {
|
||||
uploadId: payload.uploadId,
|
||||
sseChannelId: job.sseChannelId,
|
||||
sseUrl: `${req.headers["x-forwarded-proto"] || req.protocol}://${req.get("host")}/upload/sse_events/${job.sseChannelId}`,
|
||||
sseUrl: `${req.headers["x-forwarded-proto"] || req.protocol}://${req.get("x-forwarded-host") ?? req.get("host")}/upload/sse_events/${job.sseChannelId}`,
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user