Make a Video Sync for Everyone

So I’ve made a simple video player workaround, where instead of uploading a video, i upload a spritesheet and an audio. This is how it looks:


(i know it feels like i’m watching a cinema)
this system is completely cleint-sided

how can I make it so the video’s at the same time for everyone, while not making the whole system completely server-sided, so your fps isn’t dependent on ping? like watching a youtube livestream, everybody sees about the same frames delayed to to their connection or something. thanks!

here is the current script for this:

local Gui = script.Parent
local SpriteSheet = Gui:WaitForChild("SpriteSheet")
local VideoSound = Gui:WaitForChild("VideoSound")

local ContentProvider = game:GetService("ContentProvider")
local TweenService = game:GetService("TweenService")

local function PlayVideo(VideoName: string, SpriteSheet: string, Audio: string, SpriteSheetSize: Vector2, HorizontalFrameCount: number, VerticalFrameCount: number, TotalFrameCount: number, FrameRate: number, Subtext: string)
	local Gui = script.Parent
	local SpriteSheetInstance = Gui:WaitForChild("SpriteSheet")
	local VideoSoundInstance = Gui:WaitForChild("VideoSound")
	local VideoMenuInstance = Gui:WaitForChild("VideoMenu")
	
	VideoMenuInstance.GroupTransparency = 1
	VideoMenuInstance.AnchorPoint = Vector2.new(0.5, 0)
	
	VideoMenuInstance.Title.Text = "Loading"
	VideoMenuInstance.Title.Author.Text = "..."
	
	local success, err = pcall(function()
		ContentProvider:PreloadAsync({
			SpriteSheet,
			Audio
		})
	end)

	if not success then
		warn("Failed to load assets: " .. err)
		return
	end
	
	SpriteSheetInstance.Image = SpriteSheet
	VideoSoundInstance.SoundId = Audio
	
	local FRAME_SIZE = Vector2.new(
		SpriteSheetSize.X / HorizontalFrameCount,
		SpriteSheetSize.Y / VerticalFrameCount
	)

	local currentFrame = 1
	VideoMenuInstance.Title.Text = VideoName
	VideoMenuInstance.Title.Author.Text = Subtext
	VideoSoundInstance:Play()
	
	task.spawn(function()
		TweenService:Create(VideoMenuInstance, TweenInfo.new(1, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out), {GroupTransparency = 0, AnchorPoint = Vector2.new(0.5, 1)}):Play()
		wait(1.5)
		TweenService:Create(VideoMenuInstance, TweenInfo.new(.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.In), {GroupTransparency = 1, AnchorPoint = Vector2.new(0.5, 0)}):Play()
	end)
	
	for i = 1, TotalFrameCount do
		local column = (currentFrame - 1) % HorizontalFrameCount
		local row = math.floor((currentFrame - 1) / HorizontalFrameCount)

		SpriteSheetInstance.ImageRectSize = FRAME_SIZE
		SpriteSheetInstance.ImageRectOffset = Vector2.new(column * FRAME_SIZE.X, row * FRAME_SIZE.Y)

		currentFrame = currentFrame + 1
		wait(1 / FrameRate)
	end

	-- Stop the video
	VideoSoundInstance:Stop()
	SpriteSheetInstance.Image = ""
end
while true do
	PlayVideo(
		"Markiplier Moment",
		"rbxassetid://123461840032880",
		"rbxassetid://124035407806695",
		Vector2.new(988, 1000),
		10,
		18,
		176,
		40,
		"Markiplier, LynxWarlord Gaming"
	)
	PlayVideo(
		"Eat a HORSE?!",
		"rbxassetid://109096932005403",
		"rbxassetid://109826360012529",
		Vector2.new(979, 1000),
		11,
		20,
		11 * 20,
		30,
		"satinden"
	)
end

U did while true do, so it won’t be care for fps lol. The only way seems to be ping related as… uh… mabye switch to client sided if the player has high ping? The time will be the same regardless of there ping, forget abt fps. Also, why no video

You could maybe keep track when the video is started with tick, and then use that to find which frame to use in the current second–but im not sure if that will work. Not entirely sure what you would do for the sound. :123:

You may store a video configs as module in maybe ReplicatedStorage

local VideoConfigs = {}

VideoConfigs["Markiplier Moment"] = {
	VideoName = "Markiplier Moment",
	SpriteSheet = "rbxassetid://123461840032880",
	Audio = "rbxassetid://124035407806695",
	SpriteSheetSize = Vector2.new(988, 1000),
	HorizontalFrameCount = 10,
	VerticalFrameCount = 18,
	TotalFrameCount = 176,
	FrameRate = 40,
	Subtext = "Markiplier, LynxWarlord Gaming"
}

return VideoConfigs

You could use a RemoteEvent to send a play video event to all clients telling them to play a particular video

-- Server side
PlayVideoEvent:FireAllClient("Markiplier Moment")

-- Client side
PlayVideoEvent.OnClientEvent:Connect(function(VideoName: string)
	local videoConfig = VideoConfigs[VideoName]
	if not videoConfig then return end

	PlayVideo(videoConfig.VideoName, videoConfig.SpriteSheet, ...)
end)

However, if player joined while a video is already playing, they’ll miss that first video play. I will suggest to use a StringValue to trigger the request instead. also use an attribute to record when the server started the video request

-- server side, put the string value in workspace or ReplicatedStorage so client can also access
local PlayingVideo = workspace.PlayingVideo :: StringValue

local config
while true do
	PlayingVideo:SetAttribute("StartTime", workspace.GetServerTimeNow())
	PlayingVideo.Value = "Markiplier Moment"
	config = VideoConfigs[PlayingVideo.Value]
	task.wait(config.TotalFrameCount * config.FrameRate) -- i think this is the video duration

	PlayingVideo.Value = "Eat a HORSE?!"
	config = VideoConfigs[PlayingVideo.Value]
	task.wait(config.TotalFrameCount * config.FrameRate)
end

-- client side
local function OnPlayingVideoChanged(VideoName: string)
	local videoConfig = VideoConfigs[VideoName]
	if not videoConfig then return end

	local startTime = PlayingVideo:GetAttribute("StartTime") or GetServerTimeNow()
	local secondElapsed = math.max(0, GetServerTimeNow() - startTime)
	PlayVideo(videoConfig.VideoName, secondElapsed, videoConfig.SpriteSheet, ...)
								  -- ^ skip to the currently playing second to sync with server
end

OnPlayingVideoChanged(PlayingVideo.Value)
PlayingVideo.Changed:Connect(OnPlayingVideoChanged)