Hey ,
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: