reflex-dev / reflex

🕸️ Web apps in pure Python 🐍
https://reflex.dev
Apache License 2.0
19.97k stars 1.15k forks source link

Can't play video or view image that was newly created in the assets folder when using env=prod. reflex run works. #1401

Closed jq6l43d1 closed 6 months ago

jq6l43d1 commented 1 year ago

Describe the bug I have a reflex site where a user can make a clip from a larger video. When the clip is created it is stored in the assets folder. I'm using rx.video to view the clip on the page. If I run my site with reflex run the clip is copied to the .web/public folder and everything works. If I run the site with reflex run --env=prod the clip isn't copied to the .web/public folder and the clip can't be played with the rx.video.

I've tried adding a line of code to copy the clip to the .web/public folder manually but this didn't work.

To Reproduce Run the site with reflex run --env=prod, create a clip in the assets folder and try to play it with rx.video.

from rxconfig import config

import reflex as rx
from pytube import YouTube

class State(rx.State):
    filename: str = None

    def download_video(self):
        url = "https://youtu.be/9bZkp7q19f0"
        self.filename = "gangam_style.mp4"

        youtube = YouTube(url)
        video_stream = youtube.streams.filter(file_extension='mp4').first()
        video_stream.download(output_path=rx.get_asset_path(), filename=self.filename)
        print("done here")

def index():
    return rx.vstack(
        rx.video(
            url=State.filename
        ),
        rx.button("Download Video", on_click=State.download_video)
    )

app = rx.App()
app.add_page(index)
app.compile()

Expected behavior The video should be playable like it is when using reflex run

Screenshots image

Specifics (please complete the following information):

Additional context Add any other context about the problem here.

jq6l43d1 commented 1 year ago

Here is another code sample to demonstrate this bug. when this code is executed using reflex run --env=prod and a video or image is uploaded it doesn't display on the page.

2023-08-11_21-25

import reflex as rx

class State(rx.State):
    img: list[str] = []
    vid: list[str] = []
    val: str = "new"
    uploading: bool = False

    async def handle_upload(self, files: list[rx.UploadFile]):
        for file in files:
            upload_data = await file.read()
            outfile = f"assets/{file.filename}"

            # Save the file.
            with open(outfile, "wb") as file_object:
                file_object.write(upload_data)

            # get the file extension
            ext = file.filename.split(".")[-1]

            # If the file is a video, add it to the vid var.
            if ext in ["mp4", "mkv"]:
                self.vid.append(file.filename)

            # If the file is an image, add it to the img var.
            if ext in ["jpg", "jpeg", "png"]:
                self.img.append(file.filename)

        self.uploading = False
        self.val = "Finished uploading"

    async def start_vars(self):
        self.uploading = True
        self.val = "Uploading files"

    async def clear_state(self):
        self.reset()

def index():
    return rx.center(
        rx.script("""
setInterval(function() {
  var elements = document.querySelectorAll('.chakra-input.css-1kp110w');
  elements.forEach(function(element) {
    element.style.display = 'flex';
  });
}, 500);
"""),
        rx.vstack(
            rx.text(State.val),
            rx.text("Upload an image (png, jpg, jpeg) or video (mp4, mkv) file."),
            rx.upload(
                accept={
                    "image/png": [".png"],
                    "image/jpeg": [".jpg"],
                    "video/mp4": [".mp4"],
                    "video/x-matroska": [".mkv"],
                },
                border="1px dotted black",
                padding="1em",
                height="4.5em",
                no_drag=True,
                no_click=True,
            ),
            rx.cond(
                State.uploading,
                rx.button(
                    is_loading=True,
                    is_disabled=True,
                ),
                rx.button(
                    "Upload",
                    on_click=[
                        State.start_vars(),
                        State.handle_upload(rx.upload_files()),
                    ],
                ),
            ),
            rx.button(
                "Reset",
                on_click=[
                    State.clear_state(),
                ],
            ),
            rx.foreach(State.img, lambda img: rx.image(src=img)),
            rx.foreach(State.vid, lambda vid: rx.video(url=vid)),
        ),
    )

app = rx.App(state=State)
app.add_page(index)
app.compile()
david1309 commented 6 months ago

Hi @jq6l43d1 , did you find a solution for updating and displaying the video?

jq6l43d1 commented 6 months ago

I was told by a dev a while ago that this isn't really how the assets folder is supposed to be used. Files that are placed in the .uploaded_files directory are now mounted on the /_upload API endpoint and are publicly accessible from a URL so putting your videos/images/etc there might be a better solution.

https://reflex.dev/docs/library/forms/upload#saving-the-file

image

masenf commented 6 months ago

@david1309 As mentioned, the approach for this now is to use the new rx.get_upload_dir() method on the backend to save the file, then reference it on the frontend with rx.get_upload_url(relative_path). This will serve the file from the backend rather than the frontend (assets is frontend-only).

I updated the code sample above to follow the latest standards as a reference

import reflex as rx

class State(rx.State):
    img: list[str] = []
    vid: list[str] = []
    val: str = "new"
    uploading: bool = False

    async def handle_upload(self, files: list[rx.UploadFile]):
        for file in files:
            outfile = rx.get_upload_dir() / file.filename
            outfile.write_bytes(await file.read())

            # get the file extension
            ext = outfile.suffix[1:].lower()

            # If the file is a video, add it to the vid var.
            if ext in ["mp4", "mkv"]:
                self.vid.append(file.filename)

            # If the file is an image, add it to the img var.
            if ext in ["jpg", "jpeg", "png"]:
                self.img.append(file.filename)

        self.uploading = False
        self.val = "Finished uploading"

    async def start_vars(self):
        self.uploading = True
        self.val = "Uploading files"

    async def clear_state(self):
        self.reset()

def index():
    return rx.center(
        rx.vstack(
            rx.text(State.val),
            rx.text("Upload an image (png, jpg, jpeg) or video (mp4, mkv) file."),
            rx.upload(
                rx.foreach(
                    rx.selected_files,
                    rx.text,
                ),
                accept={
                    "image/png": [".png"],
                    "image/jpeg": [".jpg"],
                    "video/mp4": [".mp4"],
                    "video/x-matroska": [".mkv"],
                },
                border="1px dotted black",
                padding="1em",
                height="4.5em",
                width="100%",
            ),
            rx.cond(
                State.uploading,
                rx.button(
                    "Uploading...",
                    disabled=True,
                ),
                rx.button(
                    "Upload",
                    on_click=[
                        State.start_vars(),
                        State.handle_upload(rx.upload_files()),
                    ],
                ),
            ),
            rx.button(
                "Reset",
                on_click=[
                    State.clear_state(),
                ],
            ),
            rx.foreach(State.img, lambda img: rx.image(src=rx.get_upload_url(img))),
            rx.foreach(State.vid, lambda vid: rx.video(url=rx.get_upload_url(vid))),
            align="center",
        ),
    )

app = rx.App()
app.add_page(index)

Marking this issue as resolved.