Advanced: Unlocking Panorama 360 live streaming (Help Wanted)

Hey :wave:,

I’ve recently made a new component for dash called dash-pannellum that yall can pip install. I created a post talking about it a few days ago: Dash Pannellum - Interactive 360° 🧿 Panorama Component - #4 by adamschroeder

I’ve been able to get the component to work with photo’s, tours and video and I have it partially working for live streaming over a rtmp server. However its not a consistent live stream. It will only play video between the time that rtmp_server.py connected and started to create the live_stream.flv file till the point that the live_stream.py was ran. Only way to show more video is to manually run the live_stream.py again.

Their are 3 files that make up the live stream outside of the dash-pannellum component. With relatively small amount of code needed.

the rtmp_server.py which needs to be ran first:

import asyncio
import os
import logging
from pyrtmp import StreamClosedException
from pyrtmp.flv import FLVFileWriter, FLVMediaType
from pyrtmp.session_manager import SessionManager
from pyrtmp.rtmp import SimpleRTMPController, RTMPProtocol, SimpleRTMPServer

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class RTMPController(SimpleRTMPController):
    def __init__(self):
        super().__init__()
        self.stream_path = 'live_stream.flv'

    async def on_ns_publish(self, session, message) -> None:
        session.state = FLVFileWriter(output=self.stream_path)
        await super().on_ns_publish(session, message)

    async def on_metadata(self, session, message) -> None:
        session.state.write(0, message.to_raw_meta(), FLVMediaType.OBJECT)
        await super().on_metadata(session, message)

    async def on_video_message(self, session, message) -> None:
        session.state.write(message.timestamp, message.payload, FLVMediaType.VIDEO)
        await super().on_video_message(session, message)

    async def on_audio_message(self, session, message) -> None:
        session.state.write(message.timestamp, message.payload, FLVMediaType.AUDIO)
        await super().on_audio_message(session, message)

    async def on_stream_closed(self, session: SessionManager, exception: StreamClosedException) -> None:
        session.state.close()
        await super().on_stream_closed(session, exception)

    async def on_command_message(self, session, message) -> None:
        if message.command_name in ['releaseStream', 'FCPublish', 'FCUnpublish']:
            logger.info(f"Received command: {message.command_name}")
        else:
            await super().on_command_message(session, message)


class SimpleServer(SimpleRTMPServer):
    async def create(self, host: str, port: int):
        loop = asyncio.get_event_loop()
        self.server = await loop.create_server(
            lambda: RTMPProtocol(controller=RTMPController()),
            host=host,
            port=port,
        )


async def main():
    server = SimpleServer()
    await server.create(host='0.0.0.0', port=1935)
    await server.start()
    await server.wait_closed()

if __name__ == "__main__":
    asyncio.run(main())

then you turn on the 360 camera and select live stream via rtmp which I provide with rtmp://MY-IP-ADDRESS:1935/live and start live stream.

It quickly handshakes with the rtmp server being hosted on my computer and creates a live_stream.flv file.

I then run live_stream.py to convert this file into a .mp4 which is created in the assets folder for the dash app to read with this code:

import subprocess
import time
import os
import sys

def check_ffmpeg_installed():
    try:
        subprocess.run(['ffmpeg', '-version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
        return True
    except subprocess.CalledProcessError:
        return False
    except FileNotFoundError:
        return False

def convert_flv_to_mp4():
    input_file = 'live_stream.flv'
    output_file = os.path.join('assets', 'converted_stream.mp4')

    if not check_ffmpeg_installed():
        print("Error: FFmpeg is not installed or not in your system PATH.")
        print("Please install FFmpeg and make sure it's accessible from the command line.")
        print("Installation instructions:")
        print("- On macOS: Use Homebrew with 'brew install ffmpeg'")
        print("- On Windows: Download from https://ffmpeg.org/download.html and add to PATH")
        print("- On Linux: Use your distribution's package manager, e.g., 'sudo apt-get install ffmpeg'")
        sys.exit(1)

    ffmpeg_command = [
        'ffmpeg',
        '-i', input_file,
        '-c:v', 'libx264',
        '-preset', 'ultrafast',
        '-tune', 'zerolatency',
        '-crf', '23',
        '-c:a', 'aac',
        '-b:a', '128k',
        '-ar', '44100',
        '-f', 'mp4',
        '-movflags', 'frag_keyframe+empty_moov+faststart+separate_moof+omit_tfhd_offset+default_base_moof',
        '-frag_duration', '1000000',
        output_file
    ]

    while True:
        try:
            if os.path.exists(input_file):
                process = subprocess.Popen(ffmpeg_command)
                process.wait()
            else:
                time.sleep(1)  # Wait for 1 second before checking again
        except subprocess.CalledProcessError as e:
            print(f"Error running FFmpeg: {e}")
            time.sleep(5)  # Wait for 5 seconds before retrying
        except KeyboardInterrupt:
            print("Conversion stopped by user.")
            break

if __name__ == "__main__":
    convert_flv_to_mp4()

Then I run the app.py:

import dash
from dash import html, dcc
import dash_pannellum
from dash.dependencies import Input, Output
import os
from flask import send_from_directory

app = dash.Dash(__name__, suppress_callback_exceptions=True)


# Add a route to serve video files
@app.server.route('/video/<path:path>')
def serve_video(path):
    return send_from_directory('assets', path)


video_config = {
    "sources": [
        {"src": "/video/converted_stream.mp4", "type": "video/mp4"},
    ],
}

app.layout = html.Div([
    html.Link(
        rel='stylesheet',
        href='https://cdnjs.cloudflare.com/ajax/libs/video.js/7.20.3/video-js.min.css'
    ),
    html.Script(src='https://cdnjs.cloudflare.com/ajax/libs/video.js/7.20.3/video.min.js'),
    html.Script(src='https://cdn.jsdelivr.net/npm/pannellum@2.5.6/build/pannellum.js'),
    html.Link(
        rel='stylesheet',
        href='https://cdn.jsdelivr.net/npm/pannellum@2.5.6/build/pannellum.css'
    ),
    dash_pannellum.DashPannellum(
        id='panorama',
        video=video_config,
        autoLoad=True,
        width='100%',
        height='400px',
    ),
    dcc.Interval(
        id='interval-component',
        interval=30000,  # 30 seconds
        n_intervals=0
    )
])


@app.callback(Output('panorama', 'video'),
              Input('interval-component', 'n_intervals'))
def update_video_src(n):
    return {
        "sources": [
            {"src": f"/video/converted_stream.mp4?v={n}", "type": "video/mp4"},
        ],
    }


if __name__ == '__main__':
    app.run_server(debug=True)

Like i mentioned, the live stream will only work from the point the rtmp_server.py was ran till the point the live_stream.py was ran. If I want more video for the dahs application I need to re-run the live_stream.py. I’d like to setup the code to consistently update the live stream without needing to manually run the live_stream.py each time.

curious if anyone had any ideas on how I could improve this?

Link to the github of the 360 live streaming project:

Link to the docs of the dash pannellum component I’ve built:

Link to the github of the dash pannellum component:

1 Like