增加多个url功能

main
落雨楓 3 months ago
parent f7f9425117
commit 6942f630c7

2
.gitignore vendored

@ -39,4 +39,6 @@ next-env.d.ts
movie-sync*
dist
/main
/main.exe

@ -11,10 +11,19 @@ function startNextServer() {
return new Promise((resolve, reject) => {
console.log(`Starting Next.js server on port ${nextPort}...`);
const nextProcess = spawn('npx', ['next', 'dev', '-p', nextPort.toString()], {
stdio: 'pipe',
env: { ...process.env }
});
if (process.platform === 'win32') {
// Windows 平台下使用cmd拉起npx
nextProcess = spawn('cmd.exe', ['/c', 'npx', 'next', 'dev', '-p', nextPort.toString()], {
stdio: 'pipe',
env: { ...process.env }
});
} else {
// 其他平台
nextProcess = spawn('npx', ['next', 'dev', '-p', nextPort.toString()], {
stdio: 'pipe',
env: { ...process.env }
});
}
nextProcess.stdout.on('data', (data) => {
const output = data.toString();

@ -1,13 +1,13 @@
package entities
type ClientMessage struct {
UserName string `json:"username,omitempty"`
Room string `json:"room,omitempty"`
URL string `json:"url,omitempty"`
PlayTime int64 `json:"playTime,omitempty"`
Playing bool `json:"playing,omitempty"`
Payload string `json:"payload,omitempty"`
Password string `json:"password,omitempty"`
UserName string `json:"username,omitempty"`
Room string `json:"room,omitempty"`
VideoUrls []string `json:"videoUrls,omitempty"`
PlayTime int64 `json:"playTime,omitempty"`
Playing bool `json:"playing,omitempty"`
Payload string `json:"payload,omitempty"`
Password string `json:"password,omitempty"`
}
type ServerMessage struct {
@ -17,7 +17,7 @@ type ServerMessage struct {
type RoomStateChangedMessage struct {
Type string `json:"type,omitempty"` // "roomInfo", "stateChanged", etc.
ServerTime int64 `json:"serverTime,omitempty"`
URL string `json:"url,omitempty"`
VideoUrls []string `json:"videoUrls,omitempty"`
PlayTime int64 `json:"playTime,omitempty"`
Playing bool `json:"playing,omitempty"`
UserStatus []UserStatus `json:"userStatus,omitempty"`

@ -13,8 +13,8 @@ import (
type Room interface {
Name() string
SetName(name string)
SetUrl(url string)
GetUrl() string
SetUrls(urls []string)
GetUrls() []string
GetPlayTime() int64
SetPlayTime(playTime int64)
GetPlaying() bool
@ -36,7 +36,7 @@ var (
type RoomImpl struct {
name string
url string
urls []string
users map[string]User
lastUpdatePlayTime int64 // 上次更新时间
playTime int64
@ -52,12 +52,12 @@ func (r *RoomImpl) SetName(name string) {
r.name = name
}
func (r *RoomImpl) SetUrl(url string) {
r.url = url
func (r *RoomImpl) SetUrls(urls []string) {
r.urls = urls
}
func (r *RoomImpl) GetUrl() string {
return r.url
func (r *RoomImpl) GetUrls() []string {
return r.urls
}
func (r *RoomImpl) GetPlayTime() int64 {
@ -124,7 +124,7 @@ func (r *RoomImpl) BroadcastRoomState() {
go func() {
r.Broadcast("roomInfo", RoomStateChangedMessage{
ServerTime: time.Now().Unix(),
URL: r.GetUrl(),
VideoUrls: r.GetUrls(),
UserStatus: r.GetAllUserStatus(),
Playing: r.GetPlaying(),
PlayTime: r.GetPlayTime(),
@ -135,7 +135,7 @@ func (r *RoomImpl) BroadcastRoomState() {
r.Broadcast("roomInfo", RoomStateChangedMessage{
ServerTime: time.Now().Unix(),
URL: r.GetUrl(),
VideoUrls: r.GetUrls(),
UserStatus: r.GetAllUserStatus(),
Playing: r.GetPlaying(),
PlayTime: r.GetPlayTime(),

@ -17,7 +17,7 @@ func GetInfoEndpoint(client *socket.Socket, cliMsg *entities.ClientMessage) []by
if u != nil {
u.Send(entities.MessageTypeRoomInfo, entities.RoomStateChangedMessage{
ServerTime: time.Now().Unix(),
URL: r.GetUrl(),
VideoUrls: r.GetUrls(),
UserStatus: r.GetAllUserStatus(),
Playing: r.GetPlaying(),
PlayTime: r.GetPlayTime(),
@ -26,7 +26,7 @@ func GetInfoEndpoint(client *socket.Socket, cliMsg *entities.ClientMessage) []by
logrus.Warnf("user [%s] not in room [%s]", userID, room)
rawMsg, err := utils.StructToMapViaJSON(entities.RoomStateChangedMessage{
ServerTime: time.Now().Unix(),
URL: r.GetUrl(),
VideoUrls: r.GetUrls(),
UserStatus: r.GetAllUserStatus(),
Playing: r.GetPlaying(),
PlayTime: r.GetPlayTime(),

@ -19,7 +19,7 @@ func InitEndpoint(client *socket.Socket, cliMsg *entities.ClientMessage) []byte
if r, ok := entities.GetCinema().GetRoom(roomname); ok {
rawMsg, err := utils.StructToMapViaJSON(entities.RoomStateChangedMessage{
ServerTime: time.Now().Unix(),
URL: r.GetUrl(),
VideoUrls: r.GetUrls(),
UserStatus: r.GetAllUserStatus(),
Playing: r.GetPlaying(),
PlayTime: r.GetPlayTime(),
@ -32,7 +32,7 @@ func InitEndpoint(client *socket.Socket, cliMsg *entities.ClientMessage) []byte
} else {
rawMsg, err := utils.StructToMapViaJSON(entities.RoomStateChangedMessage{
ServerTime: time.Now().Unix(),
URL: "",
VideoUrls: []string{},
UserStatus: []entities.UserStatus{},
Playing: false,
PlayTime: 0,

@ -8,7 +8,7 @@ import (
)
func SetUrlEndpoint(client *socket.Socket, cliMsg *entities.ClientMessage) []byte {
room, url := cliMsg.Room, cliMsg.URL
room, videoUrls := cliMsg.Room, cliMsg.VideoUrls
userID := string(client.Id())
if r, ok := entities.GetCinema().GetRoom(room); ok {
u := r.GetUser(userID)
@ -23,7 +23,7 @@ func SetUrlEndpoint(client *socket.Socket, cliMsg *entities.ClientMessage) []byt
return nil
}
r.SetUrl(url)
r.SetUrls(videoUrls)
r.BroadcastRoomState()
}
} else {

@ -3,7 +3,7 @@ import { socket } from './socket'
import { ClientMessage, ClientUserStatus, RoomState, RoomStateChangedMessage, SetTimeMessage } from '@/lib/types/message'
import { $playerState, $userInfo, $userStatus } from '@/store/player'
import { useStore as useNanoStore } from '@nanostores/react'
import { getUserPlayTimeSeconds, unixTimestampWithOffset } from '@/lib/utils'
import { arrayEquals, getUserPlayTimeSeconds, unixTimestampWithOffset } from '@/lib/utils'
import { toast } from 'react-toastify'
import { CustomNotification } from './custom-notification'
@ -152,7 +152,7 @@ export const Player = ({ roomName }: { roomName: string }) => {
serverTimeAdjust.current = msg.serverTime ? msg.serverTime - (Date.now() / 1000) : 0
prevRoomState.current = {
url: msg.url,
videoUrls: msg.videoUrls,
playTime: msg.playTime,
playTimeUpdateTime: unixTimestampWithOffset(serverTimeAdjust.current),
playing: msg.playing,
@ -163,9 +163,9 @@ export const Player = ({ roomName }: { roomName: string }) => {
...msg.userStatus
])
if (msg.url && $playerState.value?.url !== msg.url) {
if (!arrayEquals(msg.videoUrls, $playerState.value?.videoUrls)) {
$playerState.set({
url: msg.url,
videoUrls: msg.videoUrls,
})
}
@ -251,17 +251,26 @@ export const Player = ({ roomName }: { roomName: string }) => {
<video
className="w-full aspect-video bg-slate-900 text-white font-sans overflow-hidden rounded-md ring-media-focus data-[focus]:ring-4"
title="影片"
src={playerState?.url}
crossOrigin="anonymous"
playsInline
controls
controlsList="nodownload,noseek"
preload="metadata"
onSeeked={handleSeeked}
onPlay={handlePlay}
onPause={handlePause}
onEnded={handlePause}
ref={player}
>
{playerState?.videoUrls && playerState.videoUrls.length > 0 ?
playerState.videoUrls.map((url, index) => (
<source key={index} src={url} />
)) : (
<div className="w-full h-full flex items-center justify-center text-white">
</div>
)}
video
</video>
</div>
)

@ -18,4 +18,6 @@ export const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
/>
)
}
)
)
TextArea.displayName = "TextArea"

@ -0,0 +1,80 @@
import { useSignal } from "@/lib/signal";
import { arrayEquals } from "@/lib/utils";
import { useCallback, useEffect, useMemo } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";
export interface UrlListInputProps {
value: string[];
onChange: (urls: string[]) => void;
placeholder?: string;
}
export const UrlListInput = (props: UrlListInputProps) => {
const urlList = useSignal<string[]>([]);
useEffect(() => {
if (!arrayEquals(urlList.value, props.value)) {
urlList.value = props.value;
}
}, [props.value]);
const handleInputChange = useCallback((index: number, value: string) => {
const newList = [...urlList.value];
if (index === urlList.value.length) {
// 新的空白输入框,如果有内容则添加到列表
if (value.trim() !== '') {
newList.push(value);
}
} else {
// 现有输入框
if (value.trim() === '') {
// 内容为空,删除该项
newList.splice(index, 1);
} else {
// 更新内容
newList[index] = value;
}
}
urlList.value = newList;
props.onChange(newList);
}, [props.onChange]);
const handleClear = useCallback((index: number) => {
const newList = [...urlList.value];
newList.splice(index, 1);
urlList.value = newList;
props.onChange(newList);
}, [props.onChange]);
return (
<div className="flex flex-col gap-2">
{/* 渲染现有的输入框 */}
{[...urlList.value, null].map((url, index) => (
<div key={index} className="flex items-center gap-2">
<Input
value={url || ""}
onChange={(e) => handleInputChange(index, e.target.value)}
placeholder={props.placeholder || "请输入URL"}
type="url"
name="video-url"
className="flex-1"
/>
<Button
variant="outline"
type="button"
size="sm"
onClick={() => handleClear(index)}
className="px-2"
disabled={index === urlList.value.length}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
);
};

@ -1,7 +1,7 @@
export interface ClientMessage {
username?: string;
room?: string;
url?: string;
videoUrls?: string[];
playTime?: number;
playing?: boolean;
payload?: string;
@ -33,7 +33,7 @@ export interface ServerNotificationMessage extends ServerMessage {
export interface RoomState {
/** 播放地址 */
url?: string;
videoUrls?: string[];
/** 服务器时间 */
serverTime?: number;
/** 播放时间更新时间 */
@ -60,7 +60,7 @@ export interface UserStatus extends ClientUserStatus {
export interface PlayerState {
paused?: boolean;
url?: string;
videoUrls?: string[];
playTime?: number;
inited?: boolean;
}

@ -32,4 +32,14 @@ export function getUserPlayTime(userStatus: UserStatus | undefined, offsetTime:
export function unixTimestampWithOffset(offset: number): number {
return Math.floor(Date.now() / 1000) + offset;
}
export function arrayEquals<T>(a: T[] | undefined, b: T[] | undefined): boolean {
if (a === b) return true;
if (!a || !b) return false;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}

@ -3,6 +3,7 @@ import { socket } from '@/components/socket'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { TextArea } from '@/components/ui/textarea'
import { UrlListInput } from '@/components/url-list-input'
import { UserList } from '@/components/user-list'
import { toast, ToastContainer } from 'react-toastify'
import { Badge, Tabs } from '@radix-ui/themes'
@ -16,6 +17,7 @@ import { Label } from '@/components/ui/label'
import { useSignal } from '@/lib/signal'
import Head from 'next/head'
import { CustomNotification } from '@/components/custom-notification'
import { arrayEquals } from '@/lib/utils'
export default function Page() {
const router = useRouter()
@ -24,7 +26,7 @@ export default function Page() {
const prevRoomState = useRef<RoomStateChangedMessage | undefined>()
const joinRole = useSignal<string>('user')
const urlInput = useSignal<string>('')
const urlList = useSignal<string[]>([])
const isRoomEmpty = useSignal(false)
const isJoined = useSignal(false)
const serverTimeAdjust = useSignal<number>(0)
@ -67,9 +69,9 @@ export default function Page() {
...msg.userStatus
])
if (prevRoomState.current?.url !== msg.url) {
if (!arrayEquals(prevRoomState.current?.videoUrls, msg.videoUrls)) {
// 更新视频链接
urlInput.value = msg.url || ''
urlList.value = msg.videoUrls ?? []
}
prevRoomState.current = d;
@ -145,11 +147,11 @@ export default function Page() {
}, [roomName])
const handleSetUrlClick = useCallback(() => {
$playerState.set({ ...$playerState.value, url: urlInput.value })
$playerState.set({ ...$playerState.value, videoUrls: urlList.value })
socket.emit('setUrl', {
room: roomName,
username: $userInfo.value?.username,
url: urlInput.value,
videoUrls: urlList.value,
} as ClientMessage)
}, [roomName])
@ -230,15 +232,12 @@ export default function Page() {
>{isRoomEmpty.value ? "创建房间" : "加入房间"}</Button>
</div>}
{isJoined.value && userInfo?.isAdmin && <div className='m-2 flex flex-col gap-2'>
<TextArea
name='videoUrl'
value={urlInput.value}
onChange={(e) => {
urlInput.value = e.target.value
<UrlListInput
value={urlList.value}
onChange={(urls) => {
urlList.value = urls
}}
autoComplete='url'
rows={3}
placeholder='视频链接,一行一个,填写多个时会从上到下尝试读取'
placeholder='视频链接'
/>
<Button onClick={handleSetUrlClick}></Button>
</div>}

Loading…
Cancel
Save