Repository: streamlink/streamlink

Issue:
Youtube Livestream download starts then instantly closes
*Thanks for reporting an issue!*
*Please read the contribution guidelines first! (see the link above)*
*Also check the list of known issues before reporting an issue!*

*Feel free to use the following template. Be as detailed as possible.*
*Please see the text preview to avoid unnecessary formatting errors.*
*Don't forget to remove this text before submitting.*

----

### Checklist

- [x] This is a bug report.
- [ ] This is a feature request.
- [ ] This is a plugin (improvement) request.
- [ ] I have read the contribution guidelines.

### Description

Youtube livestream download starts then instantly closes. Playing the file shows that it only downloaded about 1 second. The video and audio for that 1 second are fine though.

```
streamlink --hls-live-edge 99999 --hls-segment-threads 5 -o "video.ts" https://www.youtube.com/watch?v=mwhZ5C-Z1I8 best
```
This command has worked before dozens of time on the same channel. Thought it has been a few months since I last used it.
### Expected / Actual behavior

Supposed to download the entire livestream from the start but only downloads 1 second at the current moment before quitting.

### Reproduction steps / Explicit stream URLs to test

1. https://www.youtube.com/watch?v=mwhZ5C-Z1I8

### Logs

_Logs are always required for a bug report, use `-l debug` [(help)](https://streamlink.github.io/cli.html#cmdoption-l)
Make sure to **remove username and password**
You can upload your logs to https://gist.github.com/ or_

```
[Streamlink for Windows v0.10.0 - Git b839cfd]
[cli][info] Found matching plugin youtube for URL https://www.youtube.com/watch?v=mwhZ5C-Z1I8
[plugin.youtube][debug] get_video_info - 1: Found data
[cli][info] Available streams: audio_mp4, 144p (worst), 240p, 360p, 480p, 720p, 1080p, 1080p60 (best)
[cli][info] Opening stream: 1080p60 (muxed-stream)
[stream.][debug] Opening http substream
[stream.][debug] Opening http substream
[stream.mp4mux-ffmpeg][debug] ffmpeg command: \streamlink-portable-master\Streamlink for Windows (Compiled)\Releases\Streamlink\Dependencies\ffmpeg\ffmpeg.exe -nostats -y -i \\.\pipe\ffmpeg-7860-872 -i \\.\pipe\ffmpeg-7860-444 -c:v copy -c:a copy -map 0 -map 1 -f matroska pipe:1
[stream.mp4mux-ffmpeg][debug] Starting copy to pipe: \\.\pipe\ffmpeg-7860-872
[stream.mp4mux-ffmpeg][debug] Starting copy to pipe: \\.\pipe\ffmpeg-7860-444
[cli][debug] Pre-buffering 8192 bytes
[cli][debug] Checking file output
[cli][debug] Writing stream to output
[stream.mp4mux-ffmpeg][debug] Pipe copy complete: \\.\pipe\ffmpeg-7860-444
[stream.mp4mux-ffmpeg][debug] Pipe copy complete: \\.\pipe\ffmpeg-7860-872

[stream.mp4mux-ffmpeg][debug] Closing ffmpeg thread
[stream.mp4mux-ffmpeg][debug] Closed all the substreams
[cli][info] Stream ended
[cli][info] Closing currently open stream...
[stream.mp4mux-ffmpeg][debug] Closing ffmpeg thread
[stream.mp4mux-ffmpeg][debug] Closed all the substreams
[End of Streamlink for Windows with ExitCode 0]
```

### Comments, screenshots, etc.

...


[Love Streamlink? Please consider supporting our collective. Thanks!](https://opencollective.com/streamlink/donate)



Discussion:
the HTTPStream url is just 5 sec long,
it won't continue without a refresh but the refresh is 5 sec as well.

https://github.com/streamlink/streamlink/blob/5d81d68c7a47e931f050632ae7cddb3b044971b4/src/streamlink/plugins/youtube.py#L288-L289

the behavior was maybe different before.

---

the `muxed stream` _1080p60_ won't work,
but you could try a `hls stream` _1080p_ for now.

Resolving patch:
diff --git a/src/streamlink/plugins/youtube.py b/src/streamlink/plugins/youtube.py
--- a/src/streamlink/plugins/youtube.py
+++ b/src/streamlink/plugins/youtube.py
@@ -78,6 +78,8 @@ def parse_fmt_list(formatsmap):
         validate.optional("hlsvp"): validate.text,
         validate.optional("live_playback"): validate.transform(bool),
         validate.optional("reason"): validate.text,
+        validate.optional("livestream"): validate.text,
+        validate.optional("live_playback"): validate.text,
         "status": validate.text
     }
 )
@@ -137,7 +139,7 @@ class YouTube(Plugin):
     }
 
     @classmethod
-    def can_handle_url(self, url):
+    def can_handle_url(cls, url):
         return _url_re.match(url)
 
     @classmethod
@@ -157,13 +159,58 @@ def stream_weight(cls, stream):
 
         return weight, group
 
+    def _create_adaptive_streams(self, info, streams, protected):
+        adaptive_streams = {}
+        best_audio_itag = None
+
+        # Extract audio streams from the DASH format list
+        for stream_info in info.get("adaptive_fmts", []):
+            if stream_info.get("s"):
+                protected = True
+                continue
+
+            stream_params = dict(parse_qsl(stream_info["url"]))
+            if "itag" not in stream_params:
+                continue
+            itag = int(stream_params["itag"])
+            # extract any high quality streams only available in adaptive formats
+            adaptive_streams[itag] = stream_info["url"]
+
+            stream_type, stream_format = stream_info["type"]
+            if stream_type == "audio":
+                stream = HTTPStream(self.session, stream_info["url"])
+                name = "audio_{0}".format(stream_format)
+                streams[name] = stream
+
+                # find the best quality audio stream m4a, opus or vorbis
+                if best_audio_itag is None or self.adp_audio[itag] > self.adp_audio[best_audio_itag]:
+                    best_audio_itag = itag
+
+        if best_audio_itag and adaptive_streams and MuxedStream.is_usable(self.session):
+            aurl = adaptive_streams[best_audio_itag]
+            for itag, name in self.adp_video.items():
+                if itag in adaptive_streams:
+                    vurl = adaptive_streams[itag]
+                    self.logger.debug("MuxedStream: v {video} a {audio} = {name}".format(
+                        audio=best_audio_itag,
+                        name=name,
+                        video=itag,
+                    ))
+                    streams[name] = MuxedStream(self.session,
+                                                HTTPStream(self.session, vurl),
+                                                HTTPStream(self.session, aurl))
+
+        return streams, protected
+
     def _find_channel_video(self):
         res = http.get(self.url)
         match = _channelid_re.search(res.text)
         if not match:
             return
 
-        return self._get_channel_video(match.group(1))
+        channel_id = match.group(1)
+        self.logger.debug("Found channel_id: {0}".format(channel_id))
+        return self._get_channel_video(channel_id)
 
     def _get_channel_video(self, channel_id):
         query = {
@@ -178,6 +225,7 @@ def _get_channel_video(self, channel_id):
 
         for video in videos:
             video_id = video["id"]["videoId"]
+            self.logger.debug("Found video_id: {0}".format(video_id))
             return video_id
 
     def _find_canonical_stream_info(self):
@@ -233,10 +281,16 @@ def _get_stream_info(self, url):
         return info_parsed
 
     def _get_streams(self):
+        is_live = False
+
         info = self._get_stream_info(self.url)
         if not info:
             return
 
+        if info.get("livestream") == '1' or info.get("live_playback") == '1':
+            self.logger.debug("This video is live.")
+            is_live = True
+
         formats = info.get("fmt_list")
         streams = {}
         protected = False
@@ -253,40 +307,8 @@ def _get_streams(self):
 
             streams[name] = stream
 
-        adaptive_streams = {}
-        best_audio_itag = None
-
-        # Extract audio streams from the DASH format list
-        for stream_info in info.get("adaptive_fmts", []):
-            if stream_info.get("s"):
-                protected = True
-                continue
-
-            stream_params = dict(parse_qsl(stream_info["url"]))
-            if "itag" not in stream_params:
-                continue
-            itag = int(stream_params["itag"])
-            # extract any high quality streams only available in adaptive formats
-            adaptive_streams[itag] = stream_info["url"]
-
-            stream_type, stream_format = stream_info["type"]
-            if stream_type == "audio":
-                stream = HTTPStream(self.session, stream_info["url"])
-                name = "audio_{0}".format(stream_format)
-                streams[name] = stream
-
-                # find the best quality audio stream m4a, opus or vorbis
-                if best_audio_itag is None or self.adp_audio[itag] > self.adp_audio[best_audio_itag]:
-                    best_audio_itag = itag
-
-        if best_audio_itag and adaptive_streams and MuxedStream.is_usable(self.session):
-            aurl = adaptive_streams[best_audio_itag]
-            for itag, name in self.adp_video.items():
-                if itag in adaptive_streams:
-                    vurl = adaptive_streams[itag]
-                    streams[name] = MuxedStream(self.session,
-                                                HTTPStream(self.session, vurl),
-                                                HTTPStream(self.session, aurl))
+        if is_live is False:
+            streams, protected = self._create_adaptive_streams(info, streams, protected)
 
         hls_playlist = info.get("hlsvp")
         if hls_playlist:


Test patch:
diff --git a/tests/test_plugin_youtube.py b/tests/test_plugin_youtube.py
new file mode 100644
--- /dev/null
+++ b/tests/test_plugin_youtube.py
@@ -0,0 +1,23 @@
+import unittest
+
+from streamlink.plugins.youtube import YouTube
+
+
+class TestPluginYouTube(unittest.TestCase):
+    def test_can_handle_url(self):
+        should_match = [
+            "https://www.youtube.com/c/EXAMPLE/live",
+            "https://www.youtube.com/channel/EXAMPLE",
+            "https://www.youtube.com/v/aqz-KE-bpKQ",
+            "https://www.youtube.com/embed/aqz-KE-bpKQ",
+            "https://www.youtube.com/user/EXAMPLE/",
+            "https://www.youtube.com/watch?v=aqz-KE-bpKQ",
+        ]
+        for url in should_match:
+            self.assertTrue(YouTube.can_handle_url(url))
+
+        should_not_match = [
+            "https://www.youtube.com",
+        ]
+        for url in should_not_match:
+            self.assertFalse(YouTube.can_handle_url(url))