Repository: yt-dlp/yt-dlp

Issue:
[AfreecaTV] API returns a non-existent subdomain
### DO NOT REMOVE OR SKIP THE ISSUE TEMPLATE

- [X] I understand that I will be **blocked** if I *intentionally* remove or skip any mandatory\* field

### Checklist

- [X] I'm reporting that yt-dlp is broken on a **supported** site
- [X] I've verified that I have **updated yt-dlp to nightly or master** ([update instructions](https://github.com/yt-dlp/yt-dlp#update-channels))
- [X] I've checked that all provided URLs are playable in a browser with the same IP and same login details
- [X] I've checked that all URLs and arguments with special characters are [properly quoted or escaped](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#video-url-contains-an-ampersand--and-im-getting-some-strange-output-1-2839-or-v-is-not-recognized-as-an-internal-or-external-command)
- [X] I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and 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)
- [ ] 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

South Korea

### Provide a description that is worded well enough to be understood

The API now returns a non-existent domain `chromecast.afreeca.gscdn.com` when requested the old way. Now the `return_type` of a request to `/broad_stream_assign.html` should be a value hardcoded in the JavaScript code.

It appears to be related to #4466 . I am currently using a home internet line.
```
$ dig chromecast.afreeca.gscdn.com @1.1.1.1                             

; <<>> DiG 9.18.18-0ubuntu2.1-Ubuntu <<>> chromecast.afreeca.gscdn.com @1.1.1.1
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 23022
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;chromecast.afreeca.gscdn.com.	IN	A

;; Query time: 16 msec
;; SERVER: 1.1.1.1#53(1.1.1.1) (UDP)
;; WHEN: Sun Mar 03 11:23:40 KST 2024
;; MSG SIZE  rcvd: 57
```

### Provide verbose output that clearly demonstrates the problem

- [X] Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
- [ ] If using API, add `'verbose': True` to `YoutubeDL` params instead
- [X] Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below

### Complete Verbose Output

```shell
[debug] Command-line config: ['-Uv', 'https://play.afreecatv.com/orexxs/256904926']
[debug] Encodings: locale UTF-8, fs utf-8, pref UTF-8, out utf-8, error utf-8, screen utf-8
[debug] yt-dlp version nightly@2024.03.02.232720 from yt-dlp/yt-dlp-nightly-builds [6ad11fef6] (linux_exe)
[debug] Python 3.10.13 (CPython x86_64 64bit) - Linux-6.5.0-21-generic-x86_64-with-glibc2.38 (OpenSSL 3.2.1 30 Jan 2024, glibc 2.38)
[debug] exe versions: ffmpeg 6.0 (setts), ffprobe 6.0, rtmpdump 2.4
[debug] Optional libraries: Cryptodome-3.19.0, brotli-1.1.0, certifi-2024.02.02, mutagen-1.47.0, requests-2.31.0, secretstorage-3.3.3, sqlite3-3.45.1, urllib3-2.2.1, websockets-12.0
[debug] Proxy map: {}
[debug] Request Handlers: urllib, requests, websockets
[debug] Loaded 1834 extractors
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp-nightly-builds/releases/latest
Latest version: nightly@2024.03.02.232720 from yt-dlp/yt-dlp-nightly-builds
yt-dlp is up to date (nightly@2024.03.02.232720 from yt-dlp/yt-dlp-nightly-builds)
[afreecatv:live] Extracting URL: https://play.afreecatv.com/orexxs/256904926
[afreecatv:live] orexxs: Downloading JSON metadata
[afreecatv:live] 257023138: Downloading access token for sd stream
[afreecatv:live] 257023138: Downloading metadata for sd stream
[afreecatv:live] 257023138: Downloading access token for hd stream
[afreecatv:live] 257023138: Downloading metadata for hd stream
[afreecatv:live] 257023138: Downloading access token for hd2k stream
[afreecatv:live] 257023138: Downloading metadata for hd2k stream
[afreecatv:live] 257023138: Downloading access token for original stream
[afreecatv:live] 257023138: Downloading metadata for original stream
[afreecatv:live] 257023138: Downloading channel metadata
[debug] Formats sorted by: hasvid, ie_pref, lang, quality, res, fps, hdr:12(7), vcodec:vp9.2(10), channels, acodec, size, br, asr, proto, vext, aext, hasaud, source, id
[debug] Default format spec: best/bestvideo+bestaudio
[info] 257023138: Downloading 1 format(s): original
[debug] Invoking ffmpeg downloader on "https://chromecast.afreeca.gscdn.com/live-stmc-40/auth_playlist.m3u8?aid=.A32.7bbT56vyHM9fKZk.sUmE20nrrN6hlR5d6DDqyU0-MISTNoftt6sk55xSh_Ed-0E1Amk3WyPkgQ88VPueAenHSoQ2CYxel3yQVcBywaGKlRa2lIl51uajOWV7VQw"
[download] Destination: 왁물원 봄배너 만드는 날 2024-03-03 11_18 [257023138].mp4
[debug] ffmpeg command line: ffmpeg -y -loglevel verbose -headers 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Sec-Fetch-Mode: navigate
' -i 'https://chromecast.afreeca.gscdn.com/live-stmc-40/auth_playlist.m3u8?aid=.A32.7bbT56vyHM9fKZk.sUmE20nrrN6hlR5d6DDqyU0-MISTNoftt6sk55xSh_Ed-0E1Amk3WyPkgQ88VPueAenHSoQ2CYxel3yQVcBywaGKlRa2lIl51uajOWV7VQw' -c copy -f mpegts 'file:왁물원 봄배너 만드는 날 2024-03-03 11_18 [257023138].mp4.part'
ffmpeg version 6.0-6ubuntu1 Copyright (c) 2000-2023 the FFmpeg developers
  built with gcc 13 (Ubuntu 13.2.0-2ubuntu1)
  configuration: --prefix=/usr --extra-version=6ubuntu1 --toolchain=hardened --libdir=/usr/lib/x86_64-linux-gnu --incdir=/usr/include/x86_64-linux-gnu --arch=amd64 --enable-gpl --disable-stripping --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libdav1d --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libglslang --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librabbitmq --enable-librist --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libsvtav1 --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzimg --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --disable-sndio --enable-libjxl --enable-pocketsphinx --enable-librsvg --enable-libvpl --disable-libmfx --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-libplacebo --enable-librav1e --enable-shared
  libavutil      58.  2.100 / 58.  2.100
  libavcodec     60.  3.100 / 60.  3.100
  libavformat    60.  3.100 / 60.  3.100
  libavdevice    60.  1.100 / 60.  1.100
  libavfilter     9.  3.100 /  9.  3.100
  libswscale      7.  1.100 /  7.  1.100
  libswresample   4. 10.100 /  4. 10.100
  libpostproc    57.  1.100 / 57.  1.100
[tcp @ 0x5597fa134f40] Failed to resolve hostname chromecast.afreeca.gscdn.com: Name or service not known
https://chromecast.afreeca.gscdn.com/live-stmc-40/auth_playlist.m3u8?aid=.A32.7bbT56vyHM9fKZk.sUmE20nrrN6hlR5d6DDqyU0-MISTNoftt6sk55xSh_Ed-0E1Amk3WyPkgQ88VPueAenHSoQ2CYxel3yQVcBywaGKlRa2lIl51uajOWV7VQw: Input/output error


ERROR: ffmpeg exited with code 1
  File "yt_dlp/__main__.py", line 17, in <module>
  File "yt_dlp/__init__.py", line 1030, in main
  File "yt_dlp/__init__.py", line 1020, in _real_main
  File "yt_dlp/YoutubeDL.py", line 3556, in download
  File "yt_dlp/YoutubeDL.py", line 3531, in wrapper
  File "yt_dlp/YoutubeDL.py", line 1582, in extract_info
  File "yt_dlp/YoutubeDL.py", line 1593, in wrapper
  File "yt_dlp/YoutubeDL.py", line 1749, in __extract_info
  File "yt_dlp/YoutubeDL.py", line 1808, in process_ie_result
  File "yt_dlp/YoutubeDL.py", line 2966, in process_video_result
  File "yt_dlp/YoutubeDL.py", line 3432, in process_info
  File "yt_dlp/YoutubeDL.py", line 3153, in dl
  File "yt_dlp/downloader/common.py", line 455, in download
  File "yt_dlp/downloader/external.py", line 80, in real_download
  File "yt_dlp/YoutubeDL.py", line 1060, in report_error
  File "yt_dlp/YoutubeDL.py", line 988, in trouble
```



Discussion:
I am currently working on this issue.
the site serves me manifest URLs with a `live-global-cdn-v02.afreecatv.com` domain
I know about that. This is because the API operates differently in regions this site does not serve. To reproduce this issue, you will have to try with a Korean IP.
Additionally, in Korea, installing a P2P client is required to view videos in original quality. I am also looking into this issue now.

Resolving patch:
diff --git a/yt_dlp/extractor/afreecatv.py b/yt_dlp/extractor/afreecatv.py
--- a/yt_dlp/extractor/afreecatv.py
+++ b/yt_dlp/extractor/afreecatv.py
@@ -5,21 +5,63 @@
 from ..utils import (
     ExtractorError,
     OnDemandPagedList,
+    UserNotLive,
     date_from_str,
     determine_ext,
+    filter_dict,
     int_or_none,
-    qualities,
-    traverse_obj,
     unified_strdate,
     unified_timestamp,
-    update_url_query,
     url_or_none,
     urlencode_postdata,
     xpath_text,
 )
+from ..utils.traversal import traverse_obj
 
 
-class AfreecaTVIE(InfoExtractor):
+class AfreecaTVBaseIE(InfoExtractor):
+    _NETRC_MACHINE = 'afreecatv'
+
+    def _perform_login(self, username, password):
+        login_form = {
+            'szWork': 'login',
+            'szType': 'json',
+            'szUid': username,
+            'szPassword': password,
+            'isSaveId': 'false',
+            'szScriptVar': 'oLoginRet',
+            'szAction': '',
+        }
+
+        response = self._download_json(
+            'https://login.afreecatv.com/app/LoginAction.php', None,
+            'Logging in', data=urlencode_postdata(login_form))
+
+        _ERRORS = {
+            -4: 'Your account has been suspended due to a violation of our terms and policies.',
+            -5: 'https://member.afreecatv.com/app/user_delete_progress.php',
+            -6: 'https://login.afreecatv.com/membership/changeMember.php',
+            -8: "Hello! AfreecaTV here.\nThe username you have entered belongs to \n an account that requires a legal guardian's consent. \nIf you wish to use our services without restriction, \nplease make sure to go through the necessary verification process.",
+            -9: 'https://member.afreecatv.com/app/pop_login_block.php',
+            -11: 'https://login.afreecatv.com/afreeca/second_login.php',
+            -12: 'https://member.afreecatv.com/app/user_security.php',
+            0: 'The username does not exist or you have entered the wrong password.',
+            -1: 'The username does not exist or you have entered the wrong password.',
+            -3: 'You have entered your username/password incorrectly.',
+            -7: 'You cannot use your Global AfreecaTV account to access Korean AfreecaTV.',
+            -10: 'Sorry for the inconvenience. \nYour account has been blocked due to an unauthorized access. \nPlease contact our Help Center for assistance.',
+            -32008: 'You have failed to log in. Please contact our Help Center.',
+        }
+
+        result = int_or_none(response.get('RESULT'))
+        if result != 1:
+            error = _ERRORS.get(result, 'You have failed to log in.')
+            raise ExtractorError(
+                'Unable to login: %s said: %s' % (self.IE_NAME, error),
+                expected=True)
+
+
+class AfreecaTVIE(AfreecaTVBaseIE):
     IE_NAME = 'afreecatv'
     IE_DESC = 'afreecatv.com'
     _VALID_URL = r'''(?x)
@@ -138,44 +180,6 @@ def parse_video_key(key):
             video_key['part'] = int(m.group('part'))
         return video_key
 
-    def _perform_login(self, username, password):
-        login_form = {
-            'szWork': 'login',
-            'szType': 'json',
-            'szUid': username,
-            'szPassword': password,
-            'isSaveId': 'false',
-            'szScriptVar': 'oLoginRet',
-            'szAction': '',
-        }
-
-        response = self._download_json(
-            'https://login.afreecatv.com/app/LoginAction.php', None,
-            'Logging in', data=urlencode_postdata(login_form))
-
-        _ERRORS = {
-            -4: 'Your account has been suspended due to a violation of our terms and policies.',
-            -5: 'https://member.afreecatv.com/app/user_delete_progress.php',
-            -6: 'https://login.afreecatv.com/membership/changeMember.php',
-            -8: "Hello! AfreecaTV here.\nThe username you have entered belongs to \n an account that requires a legal guardian's consent. \nIf you wish to use our services without restriction, \nplease make sure to go through the necessary verification process.",
-            -9: 'https://member.afreecatv.com/app/pop_login_block.php',
-            -11: 'https://login.afreecatv.com/afreeca/second_login.php',
-            -12: 'https://member.afreecatv.com/app/user_security.php',
-            0: 'The username does not exist or you have entered the wrong password.',
-            -1: 'The username does not exist or you have entered the wrong password.',
-            -3: 'You have entered your username/password incorrectly.',
-            -7: 'You cannot use your Global AfreecaTV account to access Korean AfreecaTV.',
-            -10: 'Sorry for the inconvenience. \nYour account has been blocked due to an unauthorized access. \nPlease contact our Help Center for assistance.',
-            -32008: 'You have failed to log in. Please contact our Help Center.',
-        }
-
-        result = int_or_none(response.get('RESULT'))
-        if result != 1:
-            error = _ERRORS.get(result, 'You have failed to log in.')
-            raise ExtractorError(
-                'Unable to login: %s said: %s' % (self.IE_NAME, error),
-                expected=True)
-
     def _real_extract(self, url):
         video_id = self._match_id(url)
 
@@ -330,9 +334,9 @@ def _real_extract(self, url):
         return info
 
 
-class AfreecaTVLiveIE(AfreecaTVIE):  # XXX: Do not subclass from concrete IE
-
+class AfreecaTVLiveIE(AfreecaTVBaseIE):
     IE_NAME = 'afreecatv:live'
+    IE_DESC = 'afreecatv.com livestreams'
     _VALID_URL = r'https?://play\.afreeca(?:tv)?\.com/(?P<id>[^/]+)(?:/(?P<bno>\d+))?'
     _TESTS = [{
         'url': 'https://play.afreecatv.com/pyh3646/237852185',
@@ -347,77 +351,57 @@ class AfreecaTVLiveIE(AfreecaTVIE):  # XXX: Do not subclass from concrete IE
         },
         'skip': 'Livestream has ended',
     }, {
-        'url': 'http://play.afreeca.com/pyh3646/237852185',
+        'url': 'https://play.afreecatv.com/pyh3646/237852185',
         'only_matching': True,
     }, {
-        'url': 'http://play.afreeca.com/pyh3646',
+        'url': 'https://play.afreecatv.com/pyh3646',
         'only_matching': True,
     }]
 
     _LIVE_API_URL = 'https://live.afreecatv.com/afreeca/player_live_api.php'
 
-    _QUALITIES = ('sd', 'hd', 'hd2k', 'original')
-
     def _real_extract(self, url):
         broadcaster_id, broadcast_no = self._match_valid_url(url).group('id', 'bno')
-        password = self.get_param('videopassword')
+        channel_info = traverse_obj(self._download_json(
+            self._LIVE_API_URL, broadcaster_id, data=urlencode_postdata({'bid': broadcaster_id})),
+            ('CHANNEL', {dict})) or {}
 
-        info = self._download_json(self._LIVE_API_URL, broadcaster_id, fatal=False,
-                                   data=urlencode_postdata({'bid': broadcaster_id})) or {}
-        channel_info = info.get('CHANNEL') or {}
         broadcaster_id = channel_info.get('BJID') or broadcaster_id
         broadcast_no = channel_info.get('BNO') or broadcast_no
-        password_protected = channel_info.get('BPWD')
         if not broadcast_no:
-            raise ExtractorError(f'Unable to extract broadcast number ({broadcaster_id} may not be live)', expected=True)
-        if password_protected == 'Y' and password is None:
+            raise UserNotLive(video_id=broadcaster_id)
+
+        password = self.get_param('videopassword')
+        if channel_info.get('BPWD') == 'Y' and password is None:
             raise ExtractorError(
                 'This livestream is protected by a password, use the --video-password option',
                 expected=True)
 
-        formats = []
-        quality_key = qualities(self._QUALITIES)
-        for quality_str in self._QUALITIES:
-            params = {
+        aid = self._download_json(
+            self._LIVE_API_URL, broadcast_no, 'Downloading access token for stream',
+            'Unable to download access token for stream', data=urlencode_postdata(filter_dict({
                 'bno': broadcast_no,
                 'stream_type': 'common',
                 'type': 'aid',
-                'quality': quality_str,
-            }
-            if password is not None:
-                params['pwd'] = password
-            aid_response = self._download_json(
-                self._LIVE_API_URL, broadcast_no, fatal=False,
-                data=urlencode_postdata(params),
-                note=f'Downloading access token for {quality_str} stream',
-                errnote=f'Unable to download access token for {quality_str} stream')
-            aid = traverse_obj(aid_response, ('CHANNEL', 'AID'))
-            if not aid:
-                continue
-
-            stream_base_url = channel_info.get('RMD') or 'https://livestream-manager.afreecatv.com'
-            stream_info = self._download_json(
-                f'{stream_base_url}/broad_stream_assign.html', broadcast_no, fatal=False,
-                query={
-                    'return_type': channel_info.get('CDN', 'gcp_cdn'),
-                    'broad_key': f'{broadcast_no}-common-{quality_str}-hls',
-                },
-                note=f'Downloading metadata for {quality_str} stream',
-                errnote=f'Unable to download metadata for {quality_str} stream') or {}
-
-            if stream_info.get('view_url'):
-                formats.append({
-                    'format_id': quality_str,
-                    'url': update_url_query(stream_info['view_url'], {'aid': aid}),
-                    'ext': 'mp4',
-                    'protocol': 'm3u8',
-                    'quality': quality_key(quality_str),
-                })
-
-        station_info = self._download_json(
+                'quality': 'master',
+                'pwd': password,
+            })))['CHANNEL']['AID']
+
+        stream_base_url = channel_info.get('RMD') or 'https://livestream-manager.afreecatv.com'
+        stream_info = self._download_json(f'{stream_base_url}/broad_stream_assign.html', broadcast_no, query={
+            # works: gs_cdn_pc_app, gs_cdn_mobile_web, gs_cdn_pc_web
+            'return_type': 'gs_cdn_pc_app',
+            'broad_key': f'{broadcast_no}-common-master-hls',
+        }, note='Downloading metadata for stream', errnote='Unable to download metadata for stream')
+
+        formats = self._extract_m3u8_formats(
+            stream_info['view_url'], broadcast_no, 'mp4', m3u8_id='hls',
+            query={'aid': aid}, headers={'Referer': url})
+
+        station_info = traverse_obj(self._download_json(
             'https://st.afreecatv.com/api/get_station_status.php', broadcast_no,
-            query={'szBjId': broadcaster_id}, fatal=False,
-            note='Downloading channel metadata', errnote='Unable to download channel metadata') or {}
+            'Downloading channel metadata', 'Unable to download channel metadata',
+            query={'szBjId': broadcaster_id}, fatal=False), {dict}) or {}
 
         return {
             'id': broadcast_no,
@@ -427,6 +411,7 @@ def _real_extract(self, url):
             'timestamp': unified_timestamp(station_info.get('broad_start')),
             'formats': formats,
             'is_live': True,
+            'http_headers': {'Referer': url},
         }
 
 


Test patch: