Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 52 additions & 22 deletions src/player/video_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,17 @@ def volume_fade(self, target, fade_duration_sec, fade_interval):
self.fade.cancel()
self.fade.start(cur=cur, target=target, step=step, fade_interval=fade_interval, update_callback=self.set_volume)

def media_new(self, *args):
return self.__vlc_widget.instance.media_new(*args)
def media_new(self, url, http_headers=None):
media = self.__vlc_widget.instance.media_new(url)
# Add HTTP headers if provided (needed for YouTube streams)
if http_headers:
user_agent = http_headers.get("User-Agent")
if user_agent:
media.add_option(f":http-user-agent={user_agent}")
referrer = http_headers.get("Referer") or http_headers.get("Referrer")
if referrer:
media.add_option(f":http-referrer={referrer}")
return media

def set_media(self, *args):
self.__vlc_widget.player.set_media(*args)
Expand Down Expand Up @@ -231,8 +240,18 @@ def centercrop(self, video_width=None, video_height=None):
logger.debug(f"[CenterCrop] Crop geometry: {crop_geometry}")
self.__vlc_widget.player.video_set_crop_geometry(crop_geometry)

def add_audio_track(self, audio):
self.__vlc_widget.player.add_slave(vlc.MediaSlaveType(1), audio, True)
def add_audio_track(self, audio_url, http_headers=None):
# add_slave expects a URI string, not a Media object
# Set headers on the player's media so VLC uses them for audio too
media = self.__vlc_widget.player.get_media()
if media and http_headers:
user_agent = http_headers.get("User-Agent")
if user_agent:
media.add_option(f":http-user-agent={user_agent}")
referrer = http_headers.get("Referer") or http_headers.get("Referrer")
if referrer:
media.add_option(f":http-referrer={referrer}")
self.__vlc_widget.player.add_slave(vlc.MediaSlaveType(1), audio_url, True)

def _on_button_press_event(self, widget, event):
if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:
Expand Down Expand Up @@ -418,24 +437,35 @@ def data_source(self, data_source):

elif self.mode == MODE_STREAM:
source = data_source['Default']
formats = get_formats(source)
max_height = max(
self.windows, key=lambda m: m.get_geometry().height).get_geometry().height
video_url, video_width, video_height = get_optimal_video(
formats, max_height)
audio_url = get_best_audio(formats)

for monitor, window in self.windows.items():
media = window.media_new(video_url)
media.add_option("input-repeat=65535")
window.set_media(media)
if monitor.is_primary():
window.add_audio_track(audio_url)
else:
# `get_optimal_video` now might return video with audio.
media.add_option("no-audio")
window.set_position(0.0)
window.centercrop(video_width, video_height)
try:
formats = get_formats(source)
max_height = max(
self.windows, key=lambda m: m.get_geometry().height).get_geometry().height
video_url, video_width, video_height, video_headers = get_optimal_video(
formats, max_height)
audio_url, audio_headers = get_best_audio(formats)

logger.info(f"[Stream] Video URL: {video_url[:100]}...")
logger.info(f"[Stream] Audio URL: {audio_url[:100]}...")
logger.debug(f"[Stream] Video headers: {video_headers}")
logger.debug(f"[Stream] Audio headers: {audio_headers}")

for monitor, window in self.windows.items():
media = window.media_new(video_url, http_headers=video_headers)
media.add_option("input-repeat=65535")
# Add network caching for better streaming performance
media.add_option(":network-caching=3000")
window.set_media(media)
if monitor.is_primary():
window.add_audio_track(audio_url, http_headers=audio_headers)
else:
# `get_optimal_video` now might return video with audio.
media.add_option("no-audio")
window.set_position(0.0)
window.centercrop(video_width, video_height)
except Exception as e:
logger.error(f"[Stream] Failed to setup stream: {e}", exc_info=True)
raise
else:
raise ValueError("Invalid mode")

Expand Down
38 changes: 35 additions & 3 deletions src/yt_utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,37 @@
import yt_dlp as youtube_dl
import logging

logger = logging.getLogger("hidamari")


def get_formats(raw_url):
with youtube_dl.YoutubeDL({"noplaylist": True}) as ydl:
formats = ydl.extract_info(raw_url, download=False)["formats"]
info = ydl.extract_info(raw_url, download=False)
formats = info["formats"]
# Extract HTTP headers from info or formats
# Headers might be in info or in individual formats
http_headers = info.get("http_headers", {})

# If no headers in info, try to get them from the first format that has them
if not http_headers:
for fmt in formats:
fmt_headers = fmt.get("http_headers", {})
if fmt_headers:
http_headers = fmt_headers
break

# Ensure User-Agent is set (important for YouTube streams)
if "User-Agent" not in http_headers:
http_headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"

# Ensure Referer is set (YouTube checks this)
if "Referer" not in http_headers:
http_headers["Referer"] = raw_url

# Ensure each format has http_headers (use format-specific or fallback to common)
for fmt in formats:
if "http_headers" not in fmt or not fmt.get("http_headers"):
fmt["http_headers"] = http_headers
return formats


Expand All @@ -29,8 +57,10 @@ def get_best_audio(formats):
filtered = list(filter_audio(formats))
if not filtered:
filtered = list(filter_audio_video(formats))
if not filtered:
raise ValueError("No audio format found")
best = max(filtered, key=lambda x: x.get("quality", -1))
return best["url"]
return best["url"], best.get("http_headers", {})


def get_best_video(formats):
Expand All @@ -45,8 +75,10 @@ def get_optimal_video(formats, height):
filtered = list(filter_video(formats))
if not filtered:
filtered = list(filter_audio_video(formats))
if not filtered:
raise ValueError("No video format found")
best = min(filtered, key=lambda x: abs(x.get("height", 0) - height))
return best["url"], best["width"], best["height"]
return best["url"], best["width"], best["height"], best.get("http_headers", {})


if __name__ == "__main__":
Expand Down