Repository: yt-dlp/yt-dlp

Issue:
Add continuous Tiktok LIVE download support
### Checklist

- [X] I'm reporting a site feature request
- [X] I've verified that I'm running yt-dlp version **2022.04.08** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
- [x] I've checked that all provided URLs are alive and playable in a browser
- [X] I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates
- [X] I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue)
- [X] I've read about [sharing account credentials](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#are-you-willing-to-share-account-details-if-needed) and I'm willing to share it if required

### Region

Worldwide

### Example URLs

Tiktok streams aren't usually saved after livestream end and are generally short-lived. Therefore, supplying a perpetual example URL is not ideal. Therefore, I'll show you how.

When we copy a live link from Tiktok mobile, we get a link like this: `https://vt.tiktok.com/.....................`. You can get this link by going to the LIVE section on Tiktok mobile, hitting Share, and copying the link of virtually any stream. You can then share this link to your PC by using any sharing method, such as a messaging app.

When we paste that mobile link into a PC browser, Tiktok will redirect us to something like this: `https://tiktok.com/<username>/live`. If we browse a mobile link like that, information such as the current user's mobile OS, or the user ID will be included in the redirected URL after the `live` word. However, if we go to a Tiktok LIVE stream directly on PC, the information I just mentioned won't appear.

### Description

I'm requesting a feature to download a Tiktok LIVE as it's streamed live. I was able to do this by capturing the stream link using Internet Download Manager (though I bet you can do this with the browser dev tools as well). Then, I started the continuous download using ffmpeg. The stream file format is `.flv` (information from IDM) though I don't know if that's true because there are many media codecs.

### Verbose log

```shell
F:\>ffmpeg -i "https://pull-flv-l11-gcp01.tiktokcdn.com/game/stream-3281410567401898174_ld.flv?abr_pts=2064023" output.avi
ffmpeg version 5.0-full_build-www.gyan.dev Copyright (c) 2000-2022 the FFmpeg developers
  built with gcc 11.2.0 (Rev5, Built by MSYS2 project)
  configuration: --enable-gpl --enable-version3 --enable-static --disable-w32threads --disable-autodetect --enable-fontconfig --enable-iconv --enable-gnutls --enable-libxml2 --enable-gmp --enable-bzlib --enable-lzma --enable-libsnappy --enable-zlib --enable-librist --enable-libsrt --enable-libssh --enable-libzmq --enable-avisynth --enable-libbluray --enable-libcaca --enable-sdl2 --enable-libdav1d --enable-libdavs2 --enable-libuavs3d --enable-libzvbi --enable-librav1e --enable-libsvtav1 --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxavs2 --enable-libxvid --enable-libaom --enable-libopenjpeg --enable-libvpx --enable-mediafoundation --enable-libass --enable-frei0r --enable-libfreetype --enable-libfribidi --enable-libvidstab --enable-libvmaf --enable-libzimg --enable-amf --enable-cuda-llvm --enable-cuvid --enable-ffnvcodec --enable-nvdec --enable-nvenc --enable-d3d11va --enable-dxva2 --enable-libmfx --enable-libshaderc --enable-vulkan --enable-libplacebo --enable-opencl --enable-libcdio --enable-libgme --enable-libmodplug --enable-libopenmpt --enable-libopencore-amrwb --enable-libmp3lame --enable-libshine --enable-libtheora --enable-libtwolame --enable-libvo-amrwbenc --enable-libilbc --enable-libgsm --enable-libopencore-amrnb --enable-libopus --enable-libspeex --enable-libvorbis --enable-ladspa --enable-libbs2b --enable-libflite --enable-libmysofa --enable-librubberband --enable-libsoxr --enable-chromaprint
  libavutil      57. 17.100 / 57. 17.100
  libavcodec     59. 18.100 / 59. 18.100
  libavformat    59. 16.100 / 59. 16.100
  libavdevice    59.  4.100 / 59.  4.100
  libavfilter     8. 24.100 /  8. 24.100
  libswscale      6.  4.100 /  6.  4.100
  libswresample   4.  3.100 /  4.  3.100
  libpostproc    56.  3.100 / 56.  3.100
Input #0, flv, from 'https://pull-flv-l11-gcp01.tiktokcdn.com/game/stream-3281410567401898174_ld.flv?abr_pts=2064023':
  Metadata:
    encoder         : Lavf58.45.100
    mau             : tx
  Duration: 00:00:00.00, start: 2290.023000, bitrate: N/A
  Stream #0:0: Subtitle: text
  Stream #0:1: Audio: aac (LC), 44100 Hz, stereo, fltp, 128 kb/s
  Stream #0:2: Video: h264 (High), yuv420p(progressive), 640x360, 600 kb/s, 20 fps, 20 tbr, 1k tbn
Stream mapping:
  Stream #0:2 -> #0:0 (h264 (native) -> mpeg4 (native))
  Stream #0:1 -> #0:1 (aac (native) -> mp3 (libmp3lame))
Press [q] to stop, [?] for help
Output #0, avi, to 'output.avi':
  Metadata:
    mau             : tx
    ISFT            : Lavf59.16.100
  Stream #0:0: Video: mpeg4 (FMP4 / 0x34504D46), yuv420p(progressive), 640x360, q=2-31, 200 kb/s, 20 fps, 20 tbn
    Metadata:
      encoder         : Lavc59.18.100 mpeg4
    Side data:
      cpb: bitrate max/min/avg: 0/0/200000 buffer size: 0 vbv_delay: N/A
  Stream #0:1: Audio: mp3 (U[0][0][0] / 0x0055), 44100 Hz, stereo, fltp
    Metadata:
      encoder         : Lavc59.18.100 libmp3lame
frame=18295 fps= 20 q=23.6 Lsize=   38257kB time=00:15:14.78 bitrate= 342.6kbits/s speed=0.986x
video:22693kB audio:14293kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 3.432853%

F:\>yt-dlp https://www.tiktok.com/@ducbachhoa8/live --verbose
[debug] Command-line config: ['https://www.tiktok.com/@ducbachhoa8/live', '--verbose']
[debug] User config "C:\Users\Tran Bich Dung\yt-dlp.conf": ['-R', 'infinite', '--fragment-retries', 'infinite', '--concurrent-fragments', '8', '--merge-output-format', 'mkv']
[debug] Encodings: locale cp1252, fs utf-8, out utf-8, err utf-8, pref cp1252
[debug] yt-dlp version 2022.04.08 [7884ade65]
[debug] Lazy loading extractors is disabled
[debug] Python version 3.9.0 (CPython 64bit) - Windows-10-10.0.19041-SP0
[debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -bsfs
[debug] exe versions: ffmpeg 5.0-full_build-www.gyan.dev (setts), ffprobe 5.0-full_build-www.gyan.dev, phantomjs 2.1.1
[debug] Optional libraries: brotli, certifi, Cryptodome, mutagen, sqlite, websockets
[debug] Proxy map: {}
[debug] [generic] Extracting URL: https://www.tiktok.com/@ducbachhoa8/live
[generic] live: Requesting header
WARNING: [generic] Could not send HEAD request to https://www.tiktok.com/@ducbachhoa8/live: The read operation timed out
[generic] live: Downloading webpage
ERROR: [generic] Unable to download webpage: The read operation timed out (caused by timeout('The read operation timed out')); please report this issue on  https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using  yt-dlp -U
  File "C:\Users\Tran Bich Dung\AppData\Local\Programs\Python\Python39\lib\site-packages\yt_dlp\extractor\common.py", line 634, in extract
    ie_result = self._real_extract(url)
  File "C:\Users\Tran Bich Dung\AppData\Local\Programs\Python\Python39\lib\site-packages\yt_dlp\extractor\generic.py", line 2733, in _real_extract
    full_response = self._request_webpage(request, video_id)
  File "C:\Users\Tran Bich Dung\AppData\Local\Programs\Python\Python39\lib\site-packages\yt_dlp\extractor\common.py", line 778, in _request_webpage
    raise ExtractorError(errmsg, cause=err)

  File "C:\Users\Tran Bich Dung\AppData\Local\Programs\Python\Python39\lib\site-packages\yt_dlp\extractor\common.py", line 760, in _request_webpage
    return self._downloader.urlopen(url_or_request)
  File "C:\Users\Tran Bich Dung\AppData\Local\Programs\Python\Python39\lib\site-packages\yt_dlp\YoutubeDL.py", line 3578, in urlopen
    return self._opener.open(req, timeout=self._socket_timeout)
  File "C:\Users\Tran Bich Dung\AppData\Local\Programs\Python\Python39\lib\urllib\request.py", line 517, in open
    response = self._open(req, data)
  File "C:\Users\Tran Bich Dung\AppData\Local\Programs\Python\Python39\lib\urllib\request.py", line 534, in _open
    result = self._call_chain(self.handle_open, protocol, protocol +
  File "C:\Users\Tran Bich Dung\AppData\Local\Programs\Python\Python39\lib\urllib\request.py", line 494, in _call_chain
    result = func(*args)
  File "C:\Users\Tran Bich Dung\AppData\Local\Programs\Python\Python39\lib\site-packages\yt_dlp\utils.py", line 1428, in https_open
    return self.do_open(functools.partial(
  File "C:\Users\Tran Bich Dung\AppData\Local\Programs\Python\Python39\lib\urllib\request.py", line 1346, in do_open
    r = h.getresponse()
  File "C:\Users\Tran Bich Dung\AppData\Local\Programs\Python\Python39\lib\http\client.py", line 1347, in getresponse
    response.begin()
  File "C:\Users\Tran Bich Dung\AppData\Local\Programs\Python\Python39\lib\http\client.py", line 307, in begin
    version, status, reason = self._read_status()
  File "C:\Users\Tran Bich Dung\AppData\Local\Programs\Python\Python39\lib\http\client.py", line 268, in _read_status
    line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
  File "C:\Users\Tran Bich Dung\AppData\Local\Programs\Python\Python39\lib\socket.py", line 704, in readinto
    return self._sock.recv_into(b)
  File "C:\Users\Tran Bich Dung\AppData\Local\Programs\Python\Python39\lib\ssl.py", line 1241, in recv_into
    return self.read(nbytes, buffer)
  File "C:\Users\Tran Bich Dung\AppData\Local\Programs\Python\Python39\lib\ssl.py", line 1099, in read
    return self._sslobj.read(len, buffer)
socket.timeout: The read operation timed out
```



Discussion:
Note to devs: See #5510 for details on how this can be implemented

Resolving patch:
diff --git a/yt_dlp/extractor/_extractors.py b/yt_dlp/extractor/_extractors.py
--- a/yt_dlp/extractor/_extractors.py
+++ b/yt_dlp/extractor/_extractors.py
@@ -1868,6 +1868,7 @@
     TikTokEffectIE,
     TikTokTagIE,
     TikTokVMIE,
+    TikTokLiveIE,
     DouyinIE,
 )
 from .tinypic import TinyPicIE
diff --git a/yt_dlp/extractor/tiktok.py b/yt_dlp/extractor/tiktok.py
--- a/yt_dlp/extractor/tiktok.py
+++ b/yt_dlp/extractor/tiktok.py
@@ -11,6 +11,7 @@
     HEADRequest,
     LazyList,
     UnsupportedError,
+    UserNotLive,
     get_element_by_id,
     get_first,
     int_or_none,
@@ -980,3 +981,42 @@ def _real_extract(self, url):
         if self.suitable(new_url):  # Prevent infinite loop in case redirect fails
             raise UnsupportedError(new_url)
         return self.url_result(new_url)
+
+
+class TikTokLiveIE(InfoExtractor):
+    _VALID_URL = r'https?://(?:www\.)?tiktok\.com/@(?P<id>[\w\.-]+)/live'
+    IE_NAME = 'tiktok:live'
+
+    _TESTS = [{
+        'url': 'https://www.tiktok.com/@iris04201/live',
+        'only_matching': True,
+    }]
+
+    def _real_extract(self, url):
+        uploader = self._match_id(url)
+        webpage = self._download_webpage(url, uploader, headers={'User-Agent': 'User-Agent:Mozilla/5.0'})
+        room_id = self._html_search_regex(r'snssdk\d*://live\?room_id=(\d+)', webpage, 'room ID', default=None)
+        if not room_id:
+            raise UserNotLive(video_id=uploader)
+        live_info = traverse_obj(self._download_json(
+            'https://www.tiktok.com/api/live/detail/', room_id, query={
+                'aid': '1988',
+                'roomID': room_id,
+            }), 'LiveRoomInfo', expected_type=dict, default={})
+
+        if 'status' not in live_info:
+            raise ExtractorError('Unexpected response from TikTok API')
+        # status = 2 if live else 4
+        if not int_or_none(live_info['status']) == 2:
+            raise UserNotLive(video_id=uploader)
+
+        return {
+            'id': room_id,
+            'title': live_info.get('title') or self._html_search_meta(['og:title', 'twitter:title'], webpage, default=''),
+            'uploader': uploader,
+            'uploader_id': traverse_obj(live_info, ('ownerInfo', 'id')),
+            'creator': traverse_obj(live_info, ('ownerInfo', 'nickname')),
+            'concurrent_view_count': traverse_obj(live_info, ('liveRoomStats', 'userCount'), expected_type=int),
+            'formats': self._extract_m3u8_formats(live_info['liveUrl'], room_id, 'mp4', live=True),
+            'is_live': True,
+        }


Test patch: