diff --git a/app/build/reports/problems/problems-report.html b/app/build/reports/problems/problems-report.html index 5febd65..c91dc01 100644 --- a/app/build/reports/problems/problems-report.html +++ b/app/build/reports/problems/problems-report.html @@ -650,7 +650,7 @@ code + .copy-button { diff --git a/app/frontend/src/main/java/com/youtubeapp/data/ApiClient.kt b/app/frontend/src/main/java/com/youtubeapp/data/ApiClient.kt index 65a051a..8427460 100644 --- a/app/frontend/src/main/java/com/youtubeapp/data/ApiClient.kt +++ b/app/frontend/src/main/java/com/youtubeapp/data/ApiClient.kt @@ -5,7 +5,7 @@ import retrofit2.converter.gson.GsonConverterFactory object ApiClient { // Server-IP hier anpassen - const val BASE_URL = "http://192.168.178.92:8000/" + const val BASE_URL = "http://marha.local:8000/" val api: VideoApi by lazy { Retrofit.Builder() diff --git a/app/frontend/src/main/java/com/youtubeapp/ui/navigation/AppNavigation.kt b/app/frontend/src/main/java/com/youtubeapp/ui/navigation/AppNavigation.kt index 1148d47..92d090a 100644 --- a/app/frontend/src/main/java/com/youtubeapp/ui/navigation/AppNavigation.kt +++ b/app/frontend/src/main/java/com/youtubeapp/ui/navigation/AppNavigation.kt @@ -157,7 +157,7 @@ fun AppNavigation() { arguments = listOf(navArgument("videoId") { type = NavType.IntType }) ) { backStackEntry -> val videoId = backStackEntry.arguments?.getInt("videoId") ?: return@composable - VideoPlayerScreen(videoId = videoId, viewModel = viewModel) + VideoPlayerScreen(videoId = videoId, viewModel = viewModel, onBack = { navController.popBackStack() }) } } } diff --git a/app/frontend/src/main/java/com/youtubeapp/ui/screens/VideoPlayerScreen.kt b/app/frontend/src/main/java/com/youtubeapp/ui/screens/VideoPlayerScreen.kt index 622e2f6..15a4f7e 100644 --- a/app/frontend/src/main/java/com/youtubeapp/ui/screens/VideoPlayerScreen.kt +++ b/app/frontend/src/main/java/com/youtubeapp/ui/screens/VideoPlayerScreen.kt @@ -2,12 +2,21 @@ package com.youtubeapp.ui.screens import android.app.Activity import android.view.LayoutInflater +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat @@ -20,7 +29,7 @@ import com.youtubeapp.ui.viewmodel.VideoViewModel @Composable @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) -fun VideoPlayerScreen(videoId: Int, viewModel: VideoViewModel) { +fun VideoPlayerScreen(videoId: Int, viewModel: VideoViewModel, onBack: () -> Unit) { val context = LocalContext.current val playbackUri = viewModel.getPlaybackUri(videoId) @@ -49,15 +58,29 @@ fun VideoPlayerScreen(videoId: Int, viewModel: VideoViewModel) { } } - AndroidView( - factory = { ctx -> - val view = LayoutInflater.from(ctx).inflate(R.layout.player_view, null) as PlayerView - view.player = exoPlayer - view - }, - update = { view -> - view.player = exoPlayer - }, - modifier = Modifier.fillMaxSize() - ) + Box(modifier = Modifier.fillMaxSize()) { + AndroidView( + factory = { ctx -> + val view = LayoutInflater.from(ctx).inflate(R.layout.player_view, null) as PlayerView + view.player = exoPlayer + view + }, + update = { view -> + view.player = exoPlayer + }, + modifier = Modifier.fillMaxSize() + ) + IconButton( + onClick = onBack, + modifier = Modifier + .align(Alignment.TopStart) + .padding(16.dp) + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Zurück", + tint = Color.White + ) + } + } } diff --git a/app/frontend/src/main/java/com/youtubeapp/ui/viewmodel/VideoViewModel.kt b/app/frontend/src/main/java/com/youtubeapp/ui/viewmodel/VideoViewModel.kt index 6e8bf32..be37466 100644 --- a/app/frontend/src/main/java/com/youtubeapp/ui/viewmodel/VideoViewModel.kt +++ b/app/frontend/src/main/java/com/youtubeapp/ui/viewmodel/VideoViewModel.kt @@ -195,6 +195,10 @@ class VideoViewModel : ViewModel() { fun getPlaybackUri(videoId: Int): String { val localFile = localStorage?.getLocalFile(videoId) - return localFile?.toURI()?.toString() ?: repository.getStreamUrl(videoId) + return if (localFile != null) { + android.net.Uri.fromFile(localFile).toString() + } else { + repository.getStreamUrl(videoId) + } } } diff --git a/backend/routes/__pycache__/__init__.cpython-312.pyc b/backend/routes/__pycache__/__init__.cpython-312.pyc index 69808bf..a265420 100644 Binary files a/backend/routes/__pycache__/__init__.cpython-312.pyc and b/backend/routes/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/routes/__pycache__/videos.cpython-312.pyc b/backend/routes/__pycache__/videos.cpython-312.pyc index 7a7ca68..e48ed9e 100644 Binary files a/backend/routes/__pycache__/videos.cpython-312.pyc and b/backend/routes/__pycache__/videos.cpython-312.pyc differ diff --git a/backend/routes/videos.py b/backend/routes/videos.py index 4a294ac..72f0da2 100644 --- a/backend/routes/videos.py +++ b/backend/routes/videos.py @@ -104,7 +104,8 @@ def download_file(video_id: int, db: Session = Depends(get_db)): path = Path(video.file_path) if not path.exists(): - raise HTTPException(status_code=404, detail="Videodatei nicht gefunden") + video_service.update_file_path(db, video_id, None) + raise HTTPException(status_code=404, detail="Video noch nicht heruntergeladen") return FileResponse(path, media_type="video/mp4", filename=f"{video.title}.mp4") diff --git a/backend/services/__pycache__/__init__.cpython-312.pyc b/backend/services/__pycache__/__init__.cpython-312.pyc index 763eb5b..e6c80ea 100644 Binary files a/backend/services/__pycache__/__init__.cpython-312.pyc and b/backend/services/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/services/__pycache__/download_service.cpython-312.pyc b/backend/services/__pycache__/download_service.cpython-312.pyc index 879edb7..5d04e6b 100644 Binary files a/backend/services/__pycache__/download_service.cpython-312.pyc and b/backend/services/__pycache__/download_service.cpython-312.pyc differ diff --git a/backend/services/__pycache__/video_service.cpython-312.pyc b/backend/services/__pycache__/video_service.cpython-312.pyc index a8a520b..b792b3f 100644 Binary files a/backend/services/__pycache__/video_service.cpython-312.pyc and b/backend/services/__pycache__/video_service.cpython-312.pyc differ diff --git a/backend/services/stream_service.py b/backend/services/stream_service.py index f9577e7..99cc2b2 100644 --- a/backend/services/stream_service.py +++ b/backend/services/stream_service.py @@ -1,53 +1,91 @@ import subprocess -import time from pathlib import Path VIDEOS_DIR = "/videos" +CHUNK_SIZE = 64 * 1024 + + +def _get_stream_urls(youtube_url: str): + result = subprocess.run( + [ + "yt-dlp", + "-f", "bestvideo[ext=mp4][vcodec^=avc]+bestaudio[ext=m4a]/best[ext=mp4]", + "--print", "urls", + youtube_url, + ], + capture_output=True, text=True, timeout=30, + ) + if result.returncode != 0: + return None, None + + lines = result.stdout.strip().splitlines() + if len(lines) >= 2: + return lines[0], lines[1] + elif len(lines) == 1: + return lines[0], None + return None, None def stream_video_live(video_id: int, youtube_url: str): output_path = f"{VIDEOS_DIR}/{video_id}.mp4" - path = Path(output_path) + + video_url, audio_url = _get_stream_urls(youtube_url) + if not video_url: + return + + if audio_url: + cmd = [ + "ffmpeg", + "-reconnect", "1", + "-reconnect_streamed", "1", + "-reconnect_delay_max", "5", + "-i", video_url, + "-reconnect", "1", + "-reconnect_streamed", "1", + "-reconnect_delay_max", "5", + "-i", audio_url, + "-c:v", "copy", + "-c:a", "aac", + "-movflags", "frag_keyframe+empty_moov+default_base_moof", + "-f", "mp4", + "pipe:1", + ] + else: + cmd = [ + "ffmpeg", + "-reconnect", "1", + "-reconnect_streamed", "1", + "-reconnect_delay_max", "5", + "-i", video_url, + "-c", "copy", + "-movflags", "frag_keyframe+empty_moov+default_base_moof", + "-f", "mp4", + "pipe:1", + ] process = subprocess.Popen( - [ - "yt-dlp", - "-f", "best[ext=mp4][vcodec^=avc]/best[ext=mp4]", - "-o", output_path, - youtube_url, - ], - stdout=subprocess.DEVNULL, + cmd, + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, ) - # Warte bis Datei existiert und mindestens 1MB hat - while process.poll() is None: - if path.exists() and path.stat().st_size >= 1024 * 1024: - break - time.sleep(0.5) - - if not path.exists(): - process.wait() - return - - # Streame aus der wachsenden Datei - pos = 0 - stall_count = 0 - with open(output_path, "rb") as f: - while True: - chunk = f.read(1024 * 1024) - if chunk: - pos += len(chunk) - stall_count = 0 + try: + with open(output_path, "wb") as f: + while True: + chunk = process.stdout.read(CHUNK_SIZE) + if not chunk: + break + f.write(chunk) yield chunk - else: - if process.poll() is not None: - # Download fertig — restliche Bytes lesen - remaining = f.read() - if remaining: - yield remaining - break - stall_count += 1 - if stall_count > 60: # 30 Sekunden ohne neue Daten - break - time.sleep(0.5) + except GeneratorExit: + pass + finally: + if process.poll() is None: + process.kill() + process.wait() + if process.stdout: + process.stdout.close() + + path = Path(output_path) + if process.returncode != 0 and path.exists(): + path.unlink() diff --git a/backend/services/video_service.py b/backend/services/video_service.py index 4a78077..3734fb8 100644 --- a/backend/services/video_service.py +++ b/backend/services/video_service.py @@ -8,10 +8,11 @@ def create_video(db: Session, video_data: VideoCreate) -> Video: profile_id = video_data.profile_id data = video_data.model_dump(exclude={"profile_id"}) video = Video(**data) - if profile_id: - profile = db.query(Profile).filter(Profile.id == profile_id).first() - if profile: - video.profiles.append(profile) + if not profile_id: + profile_id = 1 + profile = db.query(Profile).filter(Profile.id == profile_id).first() + if profile: + video.profiles.append(profile) db.add(video) db.commit() db.refresh(video) diff --git a/browser_extension/background.js b/browser_extension/background.js index c35da08..0d1a737 100644 --- a/browser_extension/background.js +++ b/browser_extension/background.js @@ -1,4 +1,4 @@ -const SERVER_URL = "http://localhost:8000/videos"; +const SERVER_URL = "http://marha.local:8000/videos"; browser.runtime.onMessage.addListener((videos) => { fetch(SERVER_URL, {