[Archive] Video Player v1.1 | Play your own Video inside Roblox! Full Resolution, FPS and Audio

# Video Player v1.1

Play your own Videos in their full resolution, without lag.

This resource is going through a major rewrite due to the release of editable images. This post will stay as an “archive” of the old method used. Watch for new replies to know when the new version is released.

Table of contents:

  1. :eyes: | Showcase

  2. :scroll: | Code Usage

  3. :crystal_ball: | Future Updates

  4. :link: | Links

:eyes: | Showcase

The module uses a smart way to calculate how many frames of the video have passed since every heartbeat. It utilizes the audio track to create smooth playback, even on low-end devices. If you complete the set-up correctly, your video will never fall behind the music; it will stay synced! I conducted several testing sessions to test how reliable it is. The results are below. Take a look at its performance!

The video was shot at 1280 x 720 and 24 FPS. I compressed it so I could upload it here.

Okay, but this is on a high-end computer; what about older devices?

It’s simple; the player is smooth and optimized very well; videos shouldn’t have problems playing on mobile or bad hardware, but bad hardware is bad hardware. The video will have a noticeable lag. This is something that the Roblox Engine itself is limited to. Until Roblox fully releases their “VideoFrame” feature to the public, this is the best we can do. Refer to the archive below for the old video show on my phone. I’ll record a video with the new version soon!

Now featuring subtitle text.

A subtitle reader is now embedded in the module; you can feed it your subtitle code and it will play it alongside the video. Very useful if you are planning to make music events in your game. The video above has the English subtitles for this showcase; here is the same video, but in a different language, Russian:

Easy uploading.

The old method of importing each asset through bulk import in Studio was very painful, but no more! I have made a Python script to help. It is very easy to use. Refer to the github repository for a guide.

You can also watch this Video that I made. Sorry for the low quality and a lot of useless footage where I just waited. The video covers the full use of the module.

Notice: I had to use a random sound from the toolbox because Roblox banned the one I uploaded in the video. I also had to manually copy the IDs from the terminal because of a minor bug in the script. That bug was fixed quickly and is no longer present. If your asset name or description gets moderated, it will return HTTP Error 400.

This module can be used to create a wide variety of fun animations inside your game. You can make smooth-loading UI elements, video tutorials, gifs, and much more!

Because you are free to edit the source code for your own needs, you can expand its possibilities and create what you need.

Version 1.0 | Archived Videos

:scroll: | Code Usage

To use the module, let’s first import it. You can find it here.

  • You will see a small information message left by me and two folders. The “Raw” folder contains the raw code without any comments. If you don’t like reading them, then go straight for that.

There would be a few groups named after the directory where you should place them. The music goes to Workspace, the UI goes to StarterGUI, and the module goes to ReplicatedStorage. You can, of course, change it later if you want.

Inside the UI, you will see the so-called “Container”. This will be the UI element that you are going to use as a reference to where the frames will play. The container I have left is an example. It is simply a big frame at 1:1 scale made to fit the whole screen, but you can always change it; the usage on SurfaceGUIs has not been tested or confirmed yet.

Let’s first look at how we are going to “import” the video. Sadly, this method of playback uses a decal for every frame. If you have a longer video, you will have to import all the frames. But you can always skip every second frame, which will reduce the amount by a lot but make your video choppier. On how to actually get the frames from a video file, you can look at this YouTube Tutorial I found and used.

Head to the github repository of the asset uploader script. Follow the guide there to get it working. If you face any issues, leave a reply here or directly on GitHub.

  • You can use the Asset Manager and Bulk Import in Roblox Studio to upload them MANUALLY. As of v1.1, we are going to use the method above.

Once you have gotten all the frames from output.txt it’s time to convert them to their actual IDs. What you have currently are decalIDs, which are different from imageIDs. I have left an additional script in the module. Open it and replace the frame table with your own. After that, copy the whole script, go to the view tab on the top of your Studio, enable the command bar and output, then paste that whole script in the command bar and hit enter. It should start printing a lot of IDs; once it finishes, it will stop printing. You should see an arrow. Click it. Now you will see the whole converted table; you have to copy it. After that, simply go back to the initiator script and replace the frame table there with the new one. If you did everything correctly, the new IDs should be different from the originals.

Refer to the archive if you are using the manual method.

Version 1.0 | Archive

Now that you are done uploading your frames, we will need to add them to our table in order. Get the first frame and copy its asset ID; find the “Initiator” script inside the UI container; and find the Frames Table. Paste your ID there; now repeat this for the second, third, and so on frames until you have no more left. In the end, it should look like this:

local FrameIDs = {

13699569380, --// Your first frame




13699568397 --// Your last frame

  • If you have trouble removing rbxassetid:// then press Ctrl + F to open the Find menu. There, enter rbxassetid://, click on the small arrow in the corner, leave “Replace” empty, and press the “Replace All” button on the right.

After you are done listing the frames in a table, it’s time to format them. I have added a simple function in the module so you don’t have to. Just use it like this:

local FrameTable = VideoControl:GenerateFrames(FrameIDs)

If you don’t want to use this function, you will need to format all IDs into their rbxassetid string form, which looks like this: "rbxassetid://13699569380".

Finally, all you have to do is add your audio ID to the “Music” sound we just imported to the workspace.

As of v1.1, the preload function was improved to load faster. If you suffer from issues, please use the old one in the archive. The new version makes a thread of each “chunk” of frames to load it faster. If you don’t preload your frames, then most of them will appear as gray “ghost frames”.

local chunksLoaded = 0

for chunkIndex = 1, math.ceil(#FrameTable / 100) do


local tempTable = {}

table.move(FrameTable, chunkIndex * 100, chunkIndex * 100 + 100, 1, tempTable)

game:GetService("ContentProvider"):PreloadAsync(tempTable, function(asset, status)

if status ~= Enum.AssetFetchStatus.Success and status ~= Enum.AssetFetchStatus.Loading then




chunksLoaded += 1



repeat task.wait() until chunksLoaded == math.ceil(#FrameTable / 100)

Version 1.0 | Archive

game:GetService("ContentProvider"):PreloadAsync(FrameTable) --// Affects loading time but removes "Ghost frames".

As of v1.1, there is a new feature called subtitle code. A subtitle code reader is now embedded in the module, and you can use it. Because this might be confusing, don’t hesitate to leave a reply with your question or issue. Here is how you use it:

--// This is the subtitle code. It is very simple to use. Simply use the example below.

--// For example, at the 8th second, the text will change to "I want to be with you when the new dawn comes again,"

--// It will stay like that until the 15th second of the video, where it will change to what's next. It will continue until there is nothing left.

--// To add a time where there is no text, just put an empty string. I suggest adding an empty string at the beginning at 0 and at the end.

local Subtitles = {

{0, ""},

{8, "I want to be with you when the new dawn comes again,"},

{15, "I want to share the seconds, the joys and the sorrows."},

{22, "I'll carefully turn the page of the book where I'll"},

{32, "Collect and keep my memories."},

{35.8, "Maybe words can't tell how I feel,"},

{43, "But forgetting the fear, I'll try to take a step forward!"},

{50, "Hold out your palm and you'll soar into the sky"},

{54, "So boundless and so beautiful"},

{58, "Over the bridge of the rainbow"},

{61, "To the day to come That's waiting impatiently"},

{65, "I'll overcome my doubts, I'll find"},

{69, "What you once treasured"},

{72, "I'll be warmed by your smile and now I want to"},

{78, "Always be able to admire it!"},

{81, "One Week Friends"},

{88, ""}


Now finally use :PlayVideo(FrameTable, Container, FPS, Subtitles) and you are good to go.

Set subtitles to false if you don’t want to use them.

local Container = script.Parent -- Change this to your container.

VideoControl:PlayVideo(FrameTable, Container, 60, false)

  • And now you are done. If you did everything correctly, after a few seconds of loading the frames, your video will start. To measure performance properly, I suggest using the microProfiler; enable it with Ctrl + Shift + F6.

The folder, including the scripts with comments, has more explanation on the technical and usage parts. Please refer to them if you want to know what each function does.

If you experience any issues or have a general question, don’t hesitate to ask me. Your feedback is important to me, so I can push this small module even further.

:crystal_ball: | Future Updates

If you have any feedback, suggestions, bug reports, improvements, or optimizations, don’t hesitate to leave a reply. This is currently what I have planned for the future of this module. I will try my best to update it so it is always fresh. New suggestions will be added to the list later.

Content Check
Skip Button :x:
Play, Pause, Rewind :x:
Subtitles :white_check_mark:
  • :white_check_mark: - Added

  • :hammer: - In Development

  • :x: - Not Yet Added

🖊️ | Update Log
Video Player | v1.1


New Features

Embedded a subtitle code reader into the module.

Implemented a solution for uploading a large number of assets. Use the script here.


Changed the resample mode of each frame to Pixelated (configure it in the module)
also changed the scale type to fit, both of these changes are optional, you can remove them if the scale adds gray lines and cuts on the screen or if the resample mode makes it too pixelated or low quality on your screen

Image.ResampleMode = Enum.ResamplerMode.Pixelated --// Improves quality on larger screen resolutions. (rarely makes it worse looking)
Image.ScaleType = Enum.ScaleType.Fit --// Adds gray lines if the video does't fit the container.

Imrpoved documentation.

Better performance.

Added a FPS argument to the function to easily change it.

Removed some useless stuff that caused lag and rearranged some parts to improve readability and performance.

Merged the “Usages” and “Showcase” parts together.

I removed most details from the post so it’s easier to read.

Video Player | v1.0.1


  • I updated the post and fixed grammar mistakes. Made it more readable. Subtitles are in development. Added the “Update Log”.

:link: | Links

Get the module from here.


Play the showcase game I made to test it on your device.

Download the showcase place here:

VideoPlayerShowcase.rbxl (100.1 KB)

This is only proof of a concept. Not everything is perfect, which is why I want to share it, so we can find problems and their solutions together and create a better experience.


This is really cool! Amazing work! Might use this for some future projects.


Looks amazing! This is really nice! Never seen anything like this before!


why not use spritesheets instead of putting multiple image ids?


In short:

  • Roblox has a limitation on decal resolutions. You can, of course, use spritesheets for short GIFs or 2D character animations (from those old pixel art games).

In depth:

  • If you use spritesheets for a 1:30-minute-long video, then your quality will be very bad. There is already a post on the Dev Forum about spritesheets; this is the “other” method to allow for videos to be played, but once again, they are short and not in their full quality. I focused my module on videos at least 20 seconds long.
    You can take a look at it here:

Currently working on implementing subtitles in the module.

  • You will be able to custom code them within a table, even using translated content.

There is a small difficulty because I’m actually working on two modules: this video module and another module for music. The subtitle part was coded on the music module, and it will be a little more time-consuming for me to embed it in the video module.

I translated the showcase song into English and Russian. Take a look and listen to both versions:

Russian audiotrack and Russian subtitles.

Japanese audiotrack and English subtitles.

The subtitles use their own code and are not dependent on the audio. You can change them exactly how you want. In the future, I might also add custom properties to the code so you can change more stuff, including the color, size, etc.

This feature is not yet finished. Expect updates and improvements soon.


I took the Burger King Chicken Ad and it is fine (i was having trouble before), AND the ID gathering is time consuming


This is the only downside to this method of playback. You need to upload each frame individually. The only way to make it easier is to skip every second frame, which will not make a very noticeable difference, except if there is a lot of moving objects. As I stated you can use the asset manager and bulk import the images. You can do 200 at a time. You will get rate limited though. So you will need to either wait 24 hours or get someone else to bulk import them. I was able to upload all 2.1k frames for the showcase video in about 3 days (In those 3 days I was also coding the video player, you can do it in like 3 hours.) Also while uploading your frames, make sure there is nothing that the auto moderation will think is inappropriate. Any text needs to be blurred even if it does not violate anything, a simple 2 minute Photoshop will save you weeks of waiting for Roblox support to reply. That’s all I really have to say for now until I find a possible solution to upload them faster. As for everyone else reading this, if you know how you can upload decal images with http requests please leave a reply. I checked the API myself and couldn’t find anything related to that.


it’s impossible to get the ids quickly, and you can use alt accounts to bulk upload them, Maybe you can try the Open Cloud Assets API to upload decals?


I have searched wide before this, but I see this for the first time. This might actually be the solution. I’m going to try it out. Thanks for telling me. Time to remember my 5th grade Python knowledge :skull:


Can confirm that this does work. I couldn’t remember my Python knowledge from 5th grade so I had to remember my Javascript experience. I was able to write a script that will post an image and return you the assetID.

For anyone interested the code is below. I will make a guide on how to use it once I’m done with the “subtitles” feature. I will write a script that will automatically upload all frames from a folder and store their IDs to the table needed for the module to work. If the rate limitation is actually 60 uploads per 60 seconds then that means you will be able to upload you bad apple video in like 30 minutes, fully automatically, compared to the painful day of clicking, switching alt accounts, getting moderated, and copy pasting numbers to a table.

  • The code:
  • More in depth tutorial:

This module is pretty cool, it’s pretty tedious and all but you can get cool stuff done with it, awesome job! Heres something that we could do with the help of @bloodbonniekingnoob1 and your module :smirk_cat:
Good job with this!



  • I have made a python script to upload assets. It’s very simple to use and automatically puts everything together as it shoud. This is only a test version that is not guaranteed to work. Currently known issues: when you get your account moderated it will error. The only way to save your IDs is to copy them from the terminal output and paste them somewhere to store for further usage. You won’t be able to know how much exactly you have done, but if you want use a tool to check the amount of lines, that will be the amount of frames, remove the old ones and start uploading again. Beware, it’s easy to get banned. If you ask who exactly will be banned, it will be the user that created the API key, regardless if it’s a group or a user one.
  • How to make an API key? Go to your creator dashboard, under the credidentials tab.
    Now create a new API key, name it whatever, select API system as “Assets API” and make sure to add both read and write permissions. Add as the IP address and create your key. Make sure to copy it and to never share it. Now store it somewhere in a safe place.
  • How to use the python script? First of all, make sure you have Python installed. Open up a terminal and type python --version. If it works then you have Python. Make a new .py file, paste the script from below. Open it and paste your API key in the given position (look the comments). Now depending on who owns the API key, a group or a user, enter the group ID or your User ID of your account, make sure to change .Group() to .User() and vice versa. After that, use the video method in the links section to extract your frame. You need to put your .py file inside an empty folder, and then make a folder called “files” inside. Don’t put the .py file inside, it must stay outside the files folder. Extract your frames into that files folder. After you are done, open up a terminal, navigate to the directory using cd in terminal.
  • Now you need to install the libraries needed. Type pip install rblx-open-cloud. If pip doesn’t work for you find a guide online on how to fix your Python installation. And now simply python main.py.
  • Be patient, your connection might be slow. It takes quite a lot of time to get those images posted. After it’s done, it will stop printing new assetIDs, it will make a text file called output.txt. Open that, this is now your frames table ready to be pasted in Roblox Studio.
  • If you get banned while you are still uploading it will error, in case it was only 1 asset that was banned, copy the IDs from the terminal output and store them for later. Count the lines to know at which frame it stopped and continue later.
  • This is not the final script, I just want to share it while I’m at it for people to test and possibly find an improvement that I missed.

The Python script:

import rblxopencloud
import os

creator = rblxopencloud.Group( # Make sure to change this to .User( if you are using a userid, and to .Group( if you are using a group.
    UserID/GroupID, api_key="API_KEY" 
) # Change this to your API key you created and copied. Make sure not to share it. 

assetIDsArray = []

def uploadAsset(currentFile):
    with open(currentFile, "rb") as file:
        asset = creator.upload_asset(
            file, rblxopencloud.AssetType.Decal, "Example Asset", "Decal Description"
        ) # Feel free to change the name and description, it doesn't matter.
        if isinstance(asset, rblxopencloud.Asset):
            while True:
                status = asset.fetch_status()
                if status:

fileDirectory = './files'
for fileName in os.scandir(fileDirectory):
    if fileName.is_file():

content = "local FrameIDs = {\n"
for i in range(len(assetIDsArray)):
  content += "   " + str(assetIDsArray[i]) + ",\n"
content += "}"

with open('./output.txt', 'w') as file:

This script uses a third-party Python library called rblx-open-cloud. More information about it here:

Please share feedback from this script if you used it. I’m still not sure if it fully works. I was able to upload the first 300 frames of the bad apple video, but I’m currently experiencing some internet issues and can’t really be for sure if it did work. My frames appear as “ghost frames” and I don’t know what caused it, probably some privacy issue from permissions. If you experience the same bug, please tell me.

Use the script at your own risk. I do not take responsibility on how you use it. Don’t upload too much with it, since it’s not still confirmed that it works due to the “asset privacy” issue I have. First to try upload a few frames and confirm they do work and appear.


I figured out why the images appear gray. I made an ooppise while reading the documentaion of the Open Cloud API and thought that would upload an Image asset, but in reality it’s a decal. A decal and an Image are different. An Image type uses rbxassetid://id, while a decal can’t use that. You are still able to convert it though! I’m going to stop making new replies until I finish v1.1 with the subtitles and finished python script to upload them + a new guide on how to use it. Thanks for all compliments, @Haxonium your work is pretty cool! Would love to see how your game will look like in the future, seems interesting.
For more information how to convert a decalID to an imageID read here (before I document it in the post):

Also sorry for any grammar mistakes in my replies, I don’t have enough time at the moment. I write most stuff from my phone.


unfortunate that we have to write our own modules and screenshot a bunch of frames because roblox doesn’t want to let us use their already existing video category.


Ok I’m unsure if i misread that but you need to have each frame screenshotted, right? Because uhh Roblox studio bulk import :sob::sob::sob:


One thought I have is why you can’t “buffer” the videos, where instead of loading the entire video, instead you load only, say, the first 15 seconds, and then while those 15 seconds play, you load the next 15 seconds, and so on and so forth. I think this would help loading times a ton.


Wow! That’s really cool. I feel this is going to be used alot!


I think you got confused while reading the script. That’s literally what it does. It loads the first 400 frames of the video at once, then it starts playing them and once the frames start to become low, for example there are only 100 more frames loaded, the script will load the next frames 200, because it uses :Destroy(), the old frames are no longer rendered on the screen, so it doesn’t create more lag. Also I’m sorry for the big delay, I’m currently experiencing issues with my internet provider and there hasn’t been internet for the past few days. I will get the update done once I solve that issue. If you know Python then you won’t have an issue completing the script yourself, the hardest part was to make the new loading function work since you need to do it on the server.


I’m going to provide a small tutorial for this. I’ve got a website for getting the frames and a script that adds them to a dictionary automatically (may work on it more to skip frames that have the same id in case you accidentally inserted 2 of the same frame)