Repository: streamlink/streamlink

Issue:
 CHZZK, Naver‘s new Korean streaming plateform
### Checklist

- [X] This is a plugin request and not [a different kind of issue](https://github.com/streamlink/streamlink/issues/new/choose)
- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)
- [X] [I have checked the list of open and recently closed plugin requests](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+request%22)

### Description

A new streaming plateform from the Korean company Naver called "[CHZZK](https://chzzk.naver.com/)" is likely to quickly receive traction from Korean streamers that can‘t stay on Twitch.
It‘s still in beta though, so it might change a little, but it would be nice to explore the possibility to have a plugin started for this streaming plateform.

### Input URLs

https://chzzk.naver.com/live/17f0cfcba4ff608de5eabb5110d134d0
https://chzzk.naver.com/live/c68b8ef525fb3d2fa146344d84991753
https://chzzk.naver.com/live/4b2f27574a2e60d15bd53f94cbfc4066

Discussion:
fml09: @bastimeyer  I've fixed code lint and some exception handling, and added VOD feature. 
BTW, the validation schema rule seems to work fine, but what is your RFC saying about it?
And I didn't add the Low-Latency feature because there are a lot of things to consider, even though the service is in beta.
bastimeyer: Thanks for the update. There are still quite a few issues left though. Instead of reviewing and annotating the code issues, I decided to update the plugin and put it into proper shape myself, so it gets done quicker. I therefore force-pushed the changes onto your PR branch (which is master).

This however doesn't mean that the plugin will get merged into Streamlink's master branch now. The site is still in beta, and a working plugin doesn't change this fact.

Btw, good catch with the MPEG2 transport stream adaptation sets of the VOD DASH streams. The low latency live HLS streams are currently not supported by Streamlink, because the new HLS spec hasn't been finalized yet and partial segments have not been implemented here yet. The LLHLS playlists could be used instead, as it's backwards compatible, but it wouldn't make a difference.

----

Quick verification with a random stream and VOD:

```
$ ./script/test-plugin-urls.py chzzk -r CHANNEL_ID 4b2f27574a2e60d15bd53f94cbfc4066 -r VIDEO_ID 1856
:: Finding streams for URL: https://chzzk.naver.com/live/4b2f27574a2e60d15bd53f94cbfc4066
:: Found streams: 144p, 360p, 480p, 720p, 1080p, worst, best
:: Finding streams for URL: https://chzzk.naver.com/video/1856
:: Found streams: 144p, 480p, 720p, 1080p, worst, best
```

```
$ streamlink --json https://chzzk.naver.com/live/4b2f27574a2e60d15bd53f94cbfc4066 best | jq .metadata
{
  "id": "4144373",
  "author": "코렛트1",
  "category": "",
  "title": "치지직의 코렛트"
}
```

```
$ streamlink --json https://chzzk.naver.com/video/1856 best | jq .metadata
{
  "id": "1856",
  "author": "따효니 DDaHyoNi",
  "category": null,
  "title": "자낳대 라스트 내전."
}
```

```
$ streamlink -l debug https://chzzk.naver.com/live/4b2f27574a2e60d15bd53f94cbfc4066 best
[cli][debug] OS:         Linux-6.6.6-1-git-x86_64-with-glibc2.38
[cli][debug] Python:     3.11.6
[cli][debug] OpenSSL:    OpenSSL 3.1.4 24 Oct 2023
[cli][debug] Streamlink: 6.5.0+2.ge0008e35
[cli][debug] Dependencies:
[cli][debug]  certifi: 2023.11.17
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.9.3
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.19.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.31.0
[cli][debug]  trio: 0.23.1
[cli][debug]  trio-websocket: 0.11.1
[cli][debug]  typing-extensions: 4.8.0
[cli][debug]  urllib3: 2.1.0
[cli][debug]  websocket-client: 1.6.4
[cli][debug] Arguments:
[cli][debug]  url=https://chzzk.naver.com/live/4b2f27574a2e60d15bd53f94cbfc4066
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --player=mpv
[cli][info] Found matching plugin chzzk for URL https://chzzk.naver.com/live/4b2f27574a2e60d15bd53f94cbfc4066
[utils.l10n][debug] Language code: en_US
[stream.ffmpegmux][debug] ffmpeg version n6.1 Copyright (c) 2000-2023 the FFmpeg developers
[stream.ffmpegmux][debug]  built with gcc 13.2.1 (GCC) 20230801
[stream.ffmpegmux][debug]  configuration: --prefix=/usr --disable-debug --disable-static --disable-stripping --enable-amf --enable-avisynth --enable-cuda-llvm --enable-lto --enable-fontconfig --enable-frei0r --enable-gmp --enable-gnutls --enable-gpl --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libdav1d --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libgsm --enable-libharfbuzz --enable-libiec61883 --enable-libjack --enable-libjxl --enable-libmodplug --enable-libmp3lame --enable-libopencore_amrnb --enable-libopencore_amrwb --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libplacebo --enable-libpulse --enable-librav1e --enable-librsvg --enable-librubberband --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libsrt --enable-libssh --enable-libsvtav1 --enable-libtheora --enable-libv4l2 --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpl --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxcb --enable-libxml2 --enable-libxvid --enable-libzimg --enable-nvdec --enable-nvenc --enable-opencl --enable-opengl --enable-shared --enable-version3 --enable-vulkan
[stream.ffmpegmux][debug]  libavutil      58. 29.100 / 58. 29.100
[stream.ffmpegmux][debug]  libavcodec     60. 31.102 / 60. 31.102
[stream.ffmpegmux][debug]  libavformat    60. 16.100 / 60. 16.100
[stream.ffmpegmux][debug]  libavdevice    60.  3.100 / 60.  3.100
[stream.ffmpegmux][debug]  libavfilter     9. 12.100 /  9. 12.100
[stream.ffmpegmux][debug]  libswscale      7.  5.100 /  7.  5.100
[stream.ffmpegmux][debug]  libswresample   4. 12.100 /  4. 12.100
[stream.ffmpegmux][debug]  libpostproc    57.  3.100 / 57.  3.100
[stream.hls][debug] Using external audio tracks for stream 1080p (language=None, name=audio.stream)
[stream.hls][debug] Using external audio tracks for stream 720p (language=None, name=audio.stream)
[stream.hls][debug] Using external audio tracks for stream 480p (language=None, name=audio.stream)
[stream.hls][debug] Using external audio tracks for stream 360p (language=None, name=audio.stream)
[stream.hls][debug] Using external audio tracks for stream 144p (language=None, name=audio.stream)
[cli][info] Available streams: 144p (worst), 360p, 480p, 720p, 1080p (best)
[cli][info] Opening stream: 1080p (hls-multi)
[cli][info] Starting player: mpv
[stream.ffmpegmux][debug] Opening hls substream
[stream.hls][debug] Reloading playlist
[stream.ffmpegmux][debug] Opening hls substream
[stream.hls][debug] Reloading playlist
[utils.named_pipe][info] Creating pipe streamlinkpipe-1270134-1-5894
[utils.named_pipe][info] Creating pipe streamlinkpipe-1270134-2-6370
[stream.ffmpegmux][debug] ffmpeg command: /usr/bin/ffmpeg -nostats -y -i /tmp/streamlinkpipe-1270134-1-5894 -i /tmp/streamlinkpipe-1270134-2-6370 -c:v copy -c:a copy -map 0:v? -map 0:a? -map 1:a -copyts -f mpegts pipe:1
[stream.ffmpegmux][debug] Starting copy to pipe: /tmp/streamlinkpipe-1270134-1-5894
[stream.ffmpegmux][debug] Starting copy to pipe: /tmp/streamlinkpipe-1270134-2-6370
[cli][debug] Pre-buffering 8192 bytes
[stream.hls][debug] First Sequence: 17531; Last Sequence: 17533
[stream.hls][debug] Start offset: 0; Duration: None; Start Sequence: 17531; End Sequence: None
[stream.hls][debug] Adding segment 17531 to queue
[stream.hls][debug] Adding segment 17532 to queue
[stream.hls][debug] Adding segment 17533 to queue
[stream.hls][debug] Writing segment 17531 to output
[stream.hls][debug] Segment initialization 17531 complete
[stream.hls][debug] First Sequence: 17673; Last Sequence: 17675
[stream.hls][debug] Start offset: 0; Duration: None; Start Sequence: 17673; End Sequence: None
[stream.hls][debug] Adding segment 17673 to queue
[stream.hls][debug] Adding segment 17674 to queue
[stream.hls][debug] Adding segment 17675 to queue
[stream.hls][debug] Writing segment 17673 to output
[stream.hls][debug] Segment initialization 17673 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Writing segment 17673 to output
[stream.hls][debug] Segment 17673 complete
[stream.hls][debug] Adding segment 17534 to queue
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Writing segment 17531 to output
[stream.hls][debug] Segment 17531 complete
[cli.output][debug] Opening subprocess: ['/home/basti/.local/bin/mpv', '--force-media-title=https://chzzk.naver.com/live/4b2f27574a2e60d15bd53f94cbfc4066', '-']
[stream.hls][debug] Writing segment 17674 to output
[stream.hls][debug] Segment 17674 complete
[cli][debug] Writing stream to output
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 17676 to queue
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Writing segment 17675 to output
[stream.hls][debug] Segment 17675 complete
[stream.hls][debug] Adding segment 17535 to queue
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Writing segment 17532 to output
[stream.hls][debug] Segment 17532 complete
[stream.hls][debug] Adding segment 17677 to queue
[stream.hls][debug] Writing segment 17676 to output
[stream.hls][debug] Segment 17676 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 17536 to queue
[stream.hls][debug] Writing segment 17677 to output
[stream.hls][debug] Segment 17677 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 17678 to queue
[stream.hls][debug] Adding segment 17679 to queue
[stream.hls][debug] Writing segment 17533 to output
[stream.hls][debug] Segment 17533 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 17537 to queue
[stream.hls][debug] Writing segment 17678 to output
[stream.hls][debug] Segment 17678 complete
[cli][info] Player closed
[stream.ffmpegmux][debug] Closing ffmpeg thread
[stream.ffmpegmux][error] Error while writing to pipe /tmp/streamlinkpipe-1270134-2-6370: [Errno 32] Broken pipe
[stream.segmented][debug] Closing worker thread
[stream.segmented][debug] Closing writer thread
[stream.ffmpegmux][debug] Pipe copy complete: /tmp/streamlinkpipe-1270134-1-5894
[stream.segmented][debug] Closing worker thread
[stream.segmented][debug] Closing writer thread
[stream.ffmpegmux][debug] Closed all the substreams
[cli][info] Stream ended
[cli][info] Closing currently open stream...
```

```
$ streamlink -l debug https://chzzk.naver.com/video/1856 best
[cli][debug] OS:         Linux-6.6.6-1-git-x86_64-with-glibc2.38
[cli][debug] Python:     3.11.6
[cli][debug] OpenSSL:    OpenSSL 3.1.4 24 Oct 2023
[cli][debug] Streamlink: 6.5.0+2.ge0008e35
[cli][debug] Dependencies:
[cli][debug]  certifi: 2023.11.17
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 4.9.3
[cli][debug]  pycountry: 22.3.5
[cli][debug]  pycryptodome: 3.19.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.31.0
[cli][debug]  trio: 0.23.1
[cli][debug]  trio-websocket: 0.11.1
[cli][debug]  typing-extensions: 4.8.0
[cli][debug]  urllib3: 2.1.0
[cli][debug]  websocket-client: 1.6.4
[cli][debug] Arguments:
[cli][debug]  url=https://chzzk.naver.com/video/1856
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --player=mpv
[cli][info] Found matching plugin chzzk for URL https://chzzk.naver.com/video/1856
[utils.l10n][debug] Language code: en_US
[stream.dash][debug] Available languages for DASH audio streams: NONE (using: n/a)
[cli][info] Available streams: 144p (worst), 480p, 720p, 1080p (best)
[cli][info] Opening stream: 1080p (dash)
[cli][info] Starting player: mpv
[stream.dash][debug] Opening DASH reader for: ('69D251C533DDDEB1444A9B09557FB349DE0B', None, '7812b534-9ee7-11ee-b043-a0369ffabad8') - video/mp2t
[stream.dash.manifest][debug] Generating segment timeline for static playlist: ('69D251C533DDDEB1444A9B09557FB349DE0B', None, '7812b534-9ee7-11ee-b043-a0369ffabad8')
[cli][debug] Pre-buffering 8192 bytes
[stream.dash][debug] video/mp2t segment 0: downloading (1970-01-01T00:00:00.000000Z / 2023-12-20T21:01:08.574861Z)
[stream.dash][debug] video/mp2t segment 1: downloading (1970-01-01T00:00:00.000000Z / 2023-12-20T21:01:12.939930Z)
[stream.dash][debug] video/mp2t segment 0: completed
[cli.output][debug] Opening subprocess: ['/home/basti/.local/bin/mpv', '--force-media-title=https://chzzk.naver.com/video/1856', '-']
[cli][debug] Writing stream to output
[stream.dash][debug] video/mp2t segment 2: downloading (1970-01-01T00:00:00.000000Z / 2023-12-20T21:01:13.545367Z)
[stream.dash][debug] video/mp2t segment 1: completed
[stream.dash][debug] video/mp2t segment 3: downloading (1970-01-01T00:00:00.000000Z / 2023-12-20T21:01:14.139888Z)
[stream.dash][debug] video/mp2t segment 2: completed
[stream.dash][debug] video/mp2t segment 4: downloading (1970-01-01T00:00:00.000000Z / 2023-12-20T21:01:14.727860Z)
[stream.dash][debug] video/mp2t segment 3: completed
[stream.dash][debug] video/mp2t segment 5: downloading (1970-01-01T00:00:00.000000Z / 2023-12-20T21:01:15.325981Z)
[stream.dash][debug] video/mp2t segment 4: completed
[stream.dash][debug] video/mp2t segment 6: downloading (1970-01-01T00:00:00.000000Z / 2023-12-20T21:01:15.934503Z)
[stream.dash][debug] video/mp2t segment 5: completed
[cli][info] Player closed
[stream.segmented][debug] Closing worker thread
[stream.segmented][debug] Closing writer thread
[stream.dash][warning] video/mp2t segment 6: aborted
[cli][info] Stream ended
[cli][info] Closing currently open stream...
```
fml09: Thank you for fixing the code to be more concise. FYI, Chzzk is expected to be officially released in February. Until then, I leave this PR in draft status for the reason that there are any follow-ups, such as changes to the API specification. 
starft8900: It seems that chzzk login authentication has been added.

If logged in, information will be retrieved, but otherwise, information will not be retrieved.

![제목 없음](https://github.com/streamlink/streamlink/assets/96937577/9d536a62-44e2-4c9a-a76d-fc0a8be86454)




bastimeyer: No, they simply added a `v2` API while the plugin was still based on `v1`. This is exactly the reason why we don't merge plugins into master for sites which are still in beta. I've updated the plugin now. Streams and VODs seem to be working fine.

```
$ ./script/test-plugin-urls.py chzzk -r CHANNEL_ID fc00d47a77ed2d1156cd5997eba30310 -r VIDEO_ID 8642
:: Finding streams for URL: https://chzzk.naver.com/live/fc00d47a77ed2d1156cd5997eba30310
:: Found streams: 144p, 360p, 480p, 720p, 1080p, worst, best
:: Finding streams for URL: https://chzzk.naver.com/video/8642
:: Found streams: 144p, 480p, 720p, 1080p, worst, best
```
umemoni256: They changed more than just v1 to v2. With v2 they introduced 19+ streams (like AfreecaTV). To watch these streams, authentication is required. The name and birthday of the authenticated account has to be verified by Naver once: https://help.naver.com/service/5640/contents/20897?lang=en

19+ streams won't return the content.livePlaybackJson part in the json when https://api.chzzk.naver.com/service/v2/channels/{channel_id}/live-detail is called.
If authentication is required, content.adult is set to true in the response.

It seems GET requests to https://api.chzzk.naver.com/service/v2/channels/{channel_id}/live-detail require at least a cookie to return content.livePlaybackJson for 19+ streams. But how to get a valid cookie is unfortunately beyond my knowledge.
umemoni256: After exactly 9 hours, the stream breaks. Log is below. It seems some kind of expiration date is kicking in (hdntl=exp=1706567743 in my case). Is there anything that can be done to renew this before it expires?

`[14:35:29.238817][cli][debug] OS:         Linux-6.6.14-1-lts-x86_64-with-glibc2.38
[14:35:29.238999][cli][debug] Python:     3.11.6
[14:35:29.239139][cli][debug] OpenSSL:    OpenSSL 3.2.0 23 Nov 2023
[14:35:29.239285][cli][debug] Streamlink: 6.5.1
[14:35:29.240179][cli][debug] Dependencies:
[14:35:29.251832][cli][debug]  certifi: 2023.11.17
[14:35:29.255790][cli][debug]  isodate: 0.6.1
[14:35:29.256802][cli][debug]  lxml: 4.9.2
[14:35:29.265302][cli][debug]  pycountry: 22.3.5
[14:35:29.266242][cli][debug]  pycryptodome: 3.20.0
[14:35:29.267633][cli][debug]  PySocks: 1.7.1
[14:35:29.271091][cli][debug]  requests: 2.31.0
[14:35:29.272293][cli][debug]  trio: 0.24.0
[14:35:29.273375][cli][debug]  trio-websocket: 0.11.1
[14:35:29.278149][cli][debug]  typing-extensions: 4.9.0
[14:35:29.286692][cli][debug]  urllib3: 1.26.18
[14:35:29.303105][cli][debug]  websocket-client: 1.7.0
[14:35:29.303469][cli][debug] Arguments:
[14:35:29.303611][cli][debug]  url=https://chzzk.naver.com/live/f82903be1190262c1155674954a8ce35
[14:35:29.303734][cli][debug]  stream=['best']
[14:35:29.303868][cli][debug]  --loglevel=all
[14:35:29.304022][cli][debug]  --logfile=/data/recorder/in-progress/sl.log
[14:35:29.304163][cli][debug]  --output=/data/recorder/in-progress/f82903be1190262c1155674954a8ce35-20240129-20240129-2235-RN4293.ts
[14:35:29.304312][cli][debug]  --retry-streams=15.0
[14:35:29.304432][cli][debug]  --retry-max=60
[14:35:29.304548][cli][debug]  --retry-open=15
[14:35:29.304665][cli][debug]  --ringbuffer-size=50331648
[14:35:29.304781][cli][debug]  --stream-segment-attempts=30
[14:35:29.304896][cli][debug]  --stream-segment-threads=4
[14:35:29.305012][cli][debug]  --stream-segment-timeout=10.0
[14:35:29.305128][cli][debug]  --stream-timeout=90.0
[14:35:29.305263][cli][debug]  --hls-playlist-reload-attempts=15
[14:35:29.305379][cli][debug]  --hls-playlist-reload-time=segment
[14:35:29.305496][cli][debug]  --hls-segment-queue-threshold=15.0
[14:35:29.305623][cli][debug]  --ffmpeg-no-validation=True
[14:35:29.306068][cli][info] Found matching plugin chzzk for URL https://chzzk.naver.com/live/f82903be1190262c1155674954a8ce35
[14:35:32.432924][utils.l10n][debug] Language code: en_US
[14:35:33.922777][stream.hls.m3u8][all] #EXT-X-VERSION:7
[14:35:33.923039][stream.hls.m3u8][all] #EXT-X-INDEPENDENT-SEGMENTS
[14:35:33.923167][stream.hls.m3u8][all] #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="afrag1080p",NAME="audio.stream",AUTOSELECT=YES,CHANNELS="2",URI="hdntl=exp=1706567743~acl=*%2fkearacw3nkqwe55myd7kiecbdcfd30c9nv%2f*~data=hdntl~hmac=cd29dd926a9d1f37cb867d6e75639ec0bd1af07b1c573d2f018528e92cacf1b6/hls_chunklist_afrag1080p.m3u8"
[14:35:33.923408][stream.hls.m3u8][all] #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="afragalow.stream",NAME="audio.stream",AUTOSELECT=YES,CHANNELS="2",URI="hdntl=exp=1706567743~acl=*%2fkearacw3nkqwe55myd7kiecbdcfd30c9nv%2f*~data=hdntl~hmac=cd29dd926a9d1f37cb867d6e75639ec0bd1af07b1c573d2f018528e92cacf1b6/hls_chunklist_afragalow.stream.m3u8"
[14:35:33.923597][stream.hls.m3u8][all] #EXT-X-STREAM-INF:BANDWIDTH=8192000,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1920x1080,AUDIO="afrag1080p"
[14:35:33.923742][stream.hls.m3u8][all] hdntl=exp=1706567743~acl=*%2fkearacw3nkqwe55myd7kiecbdcfd30c9nv%2f*~data=hdntl~hmac=cd29dd926a9d1f37cb867d6e75639ec0bd1af07b1c573d2f018528e92cacf1b6/hls_chunklist_vfrag1080p.m3u8
[14:35:33.923968][stream.hls.m3u8][all] #EXT-X-STREAM-INF:BANDWIDTH=5192000,CODECS="avc1.64001F,mp4a.40.2",RESOLUTION=1280x720,AUDIO="afrag1080p"
[14:35:33.924320][stream.hls.m3u8][all] hdntl=exp=1706567743~acl=*%2fkearacw3nkqwe55myd7kiecbdcfd30c9nv%2f*~data=hdntl~hmac=cd29dd926a9d1f37cb867d6e75639ec0bd1af07b1c573d2f018528e92cacf1b6/hls_chunklist_vfrag720p.m3u8
[14:35:33.924495][stream.hls.m3u8][all] #EXT-X-STREAM-INF:BANDWIDTH=1692000,CODECS="avc1.4D401F,mp4a.40.2",RESOLUTION=852x480,AUDIO="afrag1080p"
[14:35:33.924626][stream.hls.m3u8][all] hdntl=exp=1706567743~acl=*%2fkearacw3nkqwe55myd7kiecbdcfd30c9nv%2f*~data=hdntl~hmac=cd29dd926a9d1f37cb867d6e75639ec0bd1af07b1c573d2f018528e92cacf1b6/hls_chunklist_vfrag480p.m3u8
[14:35:33.924852][stream.hls.m3u8][all] #EXT-X-STREAM-INF:BANDWIDTH=992000,CODECS="avc1.4D401E,mp4a.40.2",RESOLUTION=640x360,AUDIO="afrag1080p"
[14:35:33.924998][stream.hls.m3u8][all] hdntl=exp=1706567743~acl=*%2fkearacw3nkqwe55myd7kiecbdcfd30c9nv%2f*~data=hdntl~hmac=cd29dd926a9d1f37cb867d6e75639ec0bd1af07b1c573d2f018528e92cacf1b6/hls_chunklist_vfrag360p.m3u8
[14:35:33.925163][stream.hls.m3u8][all] #EXT-X-STREAM-INF:BANDWIDTH=192000,CODECS="avc1.4D400C,mp4a.40.2",RESOLUTION=256x144,AUDIO="afragalow.stream"
[14:35:33.925306][stream.hls.m3u8][all] hdntl=exp=1706567743~acl=*%2fkearacw3nkqwe55myd7kiecbdcfd30c9nv%2f*~data=hdntl~hmac=cd29dd926a9d1f37cb867d6e75639ec0bd1af07b1c573d2f018528e92cacf1b6/hls_chunklist_vfrag144p.m3u8
[14:35:33.925728][stream.hls][debug] Using external audio tracks for stream 1080p (language=None, name=audio.stream)
[14:35:33.925950][stream.hls][debug] Using external audio tracks for stream 720p (language=None, name=audio.stream)
[14:35:33.926125][stream.hls][debug] Using external audio tracks for stream 480p (language=None, name=audio.stream)
[14:35:33.926304][stream.hls][debug] Using external audio tracks for stream 360p (language=None, name=audio.stream)
[14:35:33.926481][stream.hls][debug] Using external audio tracks for stream 144p (language=None, name=audio.stream)
[14:35:33.927289][cli][info] Available streams: 144p (worst), 360p, 480p, 720p, 1080p (best)
[14:35:33.927472][cli][info] Opening stream: 1080p (hls-multi)
[14:35:33.927768][cli][info] Writing output to
/data/recorder/in-progress/f82903be1190262c1155674954a8ce35-20240129-2235-RN4293.ts
[14:35:33.927886][cli][debug] Checking file output
[14:35:33.928047][stream.ffmpegmux][debug] Opening hls substream
[14:35:33.928967][stream.hls][debug] Reloading playlist
[14:35:33.930755][stream.ffmpegmux][debug] Opening hls substream
[14:35:33.931497][stream.hls][debug] Reloading playlist
[14:35:33.931711][utils.named_pipe][info] Creating pipe streamlinkpipe-43041-1-6071
[14:35:33.932272][utils.named_pipe][info] Creating pipe streamlinkpipe-43041-2-8695
[14:35:33.932532][stream.ffmpegmux][debug] ffmpeg command: /usr/bin/ffmpeg -nostats -y -i /tmp/streamlinkpipe-43041-1-6071 -i /tmp/streamlinkpipe-43041-2-8695 -c:v copy -c:a copy -map 0:v? -map 0:a? -map 1:a -copyts -f mpegts pipe:1
[14:35:33.934866][stream.ffmpegmux][debug] Starting copy to pipe: /tmp/streamlinkpipe-43041-1-6071
[14:35:33.935261][stream.ffmpegmux][debug] Starting copy to pipe: /tmp/streamlinkpipe-43041-2-8695
[14:35:33.957963][cli][debug] Pre-buffering 8192 bytes
[14:35:34.224935][stream.hls][debug] First Sequence: 5; Last Sequence: 7
[14:35:34.225058][stream.hls][debug] Start offset: 0; Duration: None; Start Sequence: 5; End Sequence: None
[14:35:34.225173][stream.hls][debug] Adding segment 5 to queue
[14:35:34.227822][stream.hls][debug] Adding segment 6 to queue
[14:35:34.230830][stream.hls][debug] Adding segment 7 to queue
[14:35:34.507620][stream.hls][debug] Writing segment 5 to output
[14:35:34.507926][stream.hls][debug] Segment initialization 5 complete
...
[23:35:45.057110][stream.hls][debug] Adding segment 16343 to queue
[23:35:45.057323][stream.hls][debug] Adding segment 16344 to queue
[23:35:46.235648][stream.hls][debug] Reloading playlist
[23:35:46.747210][stream.hls][debug] Reloading playlist
[23:37:13.396492][stream.ffmpegmux][error] Error while reading from substream: Read timeout
[23:37:40.436997][stream.hls][warning] Failed to reload playlist: Unable to open URL: https://livecloud.akamaized.net/chzzk/lip2_kr2/anmss0647/kearacw3nkqwe55myd7kiecbdcfd30c9nv/hdntl=exp=1706567743~acl=*%2fkearacw3nkqwe55myd7kiecbdcfd30c9nv%2f*~data=hdntl~hmac=cd29dd926a9d1f37cb867d6e75639ec0bd1af07b1c573d2f018528e92cacf1b6/hls_chunklist_afrag1080p.m3u8 (403 Client Error: Forbidden for url: https://livecloud.akamaized.net/chzzk/lip2_kr2/anmss0647/kearacw3nkqwe55myd7kiecbdcfd30c9nv/hdntl=exp=1706567743~acl=*%2fkearacw3nkqwe55myd7kiecbdcfd30c9nv%2f*~data=hdntl~hmac=cd29dd926a9d1f37cb867d6e75639ec0bd1af07b1c573d2f018528e92cacf1b6/hls_chunklist_afrag1080p.m3u8)
[23:37:40.437613][stream.hls][warning] No new segments in playlist for more than 30.00s. Stopping...
[23:37:40.437923][stream.segmented][debug] Closing worker thread
[23:37:40.471071][stream.hls][warning] Failed to reload playlist: Unable to open URL: https://livecloud.akamaized.net/chzzk/lip2_kr2/anmss0647/kearacw3nkqwe55myd7kiecbdcfd30c9nv/hdntl=exp=1706567743~acl=*%2fkearacw3nkqwe55myd7kiecbdcfd30c9nv%2f*~data=hdntl~hmac=cd29dd926a9d1f37cb867d6e75639ec0bd1af07b1c573d2f018528e92cacf1b6/hls_chunklist_vfrag1080p.m3u8 (403 Client Error: Forbidden for url: https://livecloud.akamaized.net/chzzk/lip2_kr2/anmss0647/kearacw3nkqwe55myd7kiecbdcfd30c9nv/hdntl=exp=1706567743~acl=*%2fkearacw3nkqwe55myd7kiecbdcfd30c9nv%2f*~data=hdntl~hmac=cd29dd926a9d1f37cb867d6e75639ec0bd1af07b1c573d2f018528e92cacf1b6/hls_chunklist_vfrag1080p.m3u8)
[23:37:40.471362][stream.hls][warning] No new segments in playlist for more than 30.00s. Stopping...
[23:37:40.471547][stream.segmented][debug] Closing worker thread
[23:37:40.474213][stream.segmented][debug] Closing writer thread
[23:37:40.474751][stream.ffmpegmux][debug] Pipe copy complete: /tmp/streamlinkpipe-43041-1-6071
[23:37:45.346803][stream.ffmpegmux][debug] Closing ffmpeg thread
[23:37:45.351042][stream.segmented][debug] Closing writer thread
[23:40:12.357906][stream.hls][error] Failed to fetch segment 16343: Unable to open URL: https://livecloud.akamaized.net/chzzk/lip2_kr2/anmss0647/kearacw3nkqwe55myd7kiecbdcfd30c9nv/hdntl=exp=1706567743~acl=*%2fkearacw3nkqwe55myd7kiecbdcfd30c9nv%2f*~data=hdntl~hmac=cd29dd926a9d1f37cb867d6e75639ec0bd1af07b1c573d2f018528e92cacf1b6/afrag1080p_691305293_1706567740801_32424_0_16343.m4a?type=hls&bitrate=29132&filetype=.m4a (403 Client Error: Forbidden for url: https://livecloud.akamaized.net/chzzk/lip2_kr2/anmss0647/kearacw3nkqwe55myd7kiecbdcfd30c9nv/hdntl=exp=1706567743~acl=*%2fkearacw3nkqwe55myd7kiecbdcfd30c9nv%2f*~data=hdntl~hmac=cd29dd926a9d1f37cb867d6e75639ec0bd1af07b1c573d2f018528e92cacf1b6/afrag1080p_691305293_1706567740801_32424_0_16343.m4a?type=hls&bitrate=29132&filetype=.m4a)
[23:40:12.365906][stream.hls][error] Failed to fetch segment 16344: Unable to open URL: https://livecloud.akamaized.net/chzzk/lip2_kr2/anmss0647/kearacw3nkqwe55myd7kiecbdcfd30c9nv/hdntl=exp=1706567743~acl=*%2fkearacw3nkqwe55myd7kiecbdcfd30c9nv%2f*~data=hdntl~hmac=cd29dd926a9d1f37cb867d6e75639ec0bd1af07b1c573d2f018528e92cacf1b6/afrag1080p_2801223308_1706567742785_32426_0_16344.m4a?type=hls&bitrate=29207&filetype=.m4a (403 Client Error: Forbidden for url: https://livecloud.akamaized.net/chzzk/lip2_kr2/anmss0647/kearacw3nkqwe55myd7kiecbdcfd30c9nv/hdntl=exp=1706567743~acl=*%2fkearacw3nkqwe55myd7kiecbdcfd30c9nv%2f*~data=hdntl~hmac=cd29dd926a9d1f37cb867d6e75639ec0bd1af07b1c573d2f018528e92cacf1b6/afrag1080p_2801223308_1706567742785_32426_0_16344.m4a?type=hls&bitrate=29207&filetype=.m4a)
[23:40:12.367595][stream.ffmpegmux][debug] Closed all the substreams
[23:40:12.373256][cli][info] Stream ended
[23:40:12.375458][cli][info] Closing currently open stream...
`
hoonnn: @umemoni256,
Me too. After 9 hours, the recording stops with a 403. 😢
blluv: I think you should implement the URL property and refresh the m3u8 url before it expires.

https://github.com/streamlink/streamlink/blob/150edc902990758fbe94985a49224e8c17a14594/src/streamlink/plugins/oneplusone.py#L44-L62
fml09: @umemoni256 @hoonnn @bastimeyer Hey guys. The token value expired 9 hours after it was issued, causing the recording to cut off in the middle. I revamped the code to reissue the token 3 hours before it expired and replace it in the URL fetching on the HLS playlist. I would appreciate some testing and code review. 
bastimeyer: Sorry, but I'm not going to merge these newly added changes here. Please restore the branch again to the previous commit or I will do it once this plugin is ready to be merged.

The initial plugin implementation should be focused on the basics, which commit 150edc9 achieved just fine (unless something has changed since it was created). An expiration timeout after 9 hours is considered an edge-case. Streamlink was never built for recording streams and it's still not the focus of the project today. This is partly the reason why the stream implementations are still lacking some extensibility capabilities. Just as a quick remark, in order for Streamlink to properly support recording which then can be advertised properly, many things will need to change on the side of both the streams API and CLI implementation.

This however doesn't mean that the plugin can't be improved with refresh logic for the expiration timeout in the future. The way this is done here though is not good. I am aware that you are trying to reload the multivariant playlist, which is of course a bit tricky, but this proposed implementation, ignoring all the other linting, typing and method/function-signature issues, is having lots of issues which makes it impossible to merge. You are overriding a ton of HLSStream logic here, which makes it an unmaintainable mess once the base classes need to get updated, apart from the fact that lots of other stuff from the overridden methods is missing.

A proper implementation should make sure that the base classes are extensible with a clean API, so subclasses can apply their extended logic which modifies the HLS stream behavior.

Also, please keep the coding style of the project in mind when submitting changes. We don't have strict code **style** linters/formatters configured, but this doesn't mean that we're going to merge anything that the Python interpreter will parse. Annotating all these style issues is way too tedious. Btw, these changes here don't pass `ruff` and `mypy`.
fml09: I understand that the points you've raised. Additionally, I recognize the challenges with linting and formatting due to local environment setup issues. I appreciate your intention to review for a Proof-of-Concept without fully considering code quality.

However, I would like to clarify one thing: 
>Are you suggesting that handling the expiration timeout as an edge-case and addressing it in an ad-hoc manner is problematic?

If so, I've observed that other plugins within Streamlink that override HLSStream also deal with their expiration logic in ways that might seem tricky [[1]](https://github.com/streamlink/streamlink/blob/master/src/streamlink/plugins/filmon.py#L27-L91) [[2]](https://github.com/streamlink/streamlink/blob/master/src/streamlink/plugins/mildom.py#L41-L47) [[3]](https://github.com/streamlink/streamlink/blob/master/src/streamlink/plugins/oneplusone.py#L44-L62). Could you specify what distinguishes my approach from theirs and pinpoint where the issue lies?


fml09: @bastimeyer I resolved the issue by referring to [this PR](https://github.com/streamlink/streamlink/pull/5835)
HDSRK: Can it works in streamlink 6.2.0?
fml09: @HDSRK No, it is not supported. Only Streamlink version >=6.6.0 are supported. If you want to use it with a lower version, you can manually add the code directly from [this commit](https://github.com/streamlink/streamlink/pull/5731/commits/dc346300ed5ee97289013061fb6bcc4f4e4a7820) into the plugins directory. However, it will not be officially supported though.
bastimeyer: > Are you suggesting that handling the expiration timeout as an edge-case and addressing it in an ad-hoc manner is problematic?

No, it's not problematic, because playlist expirations are not part of the HLS specification and depend very much on how the streaming platform has implemented them. Plugins therefore always need custom logic in order to solve this type of issue. There can't be a generic solution.

What I was saying was (in the context of your previous changes) that overloading new plugins with tons of extra code is bad for edge cases like this. It's an edge case because it's an expiration timer of 9 hours in contrast to only a couple of minutes for example, which would make the plugin unusable.

I asked you to add playlist expiration logic after the basic plugin implementation has been merged, so extending it, reviewing the code and getting feedback is easier.

----

Your recent changes however already look much better, but I haven't checked it in detail yet. I can already see a couple of issues though which will require fixing. But as said, I don't want to review this here.

> How can I use validate.union_get to extract the values of properties from the array inside media? I'm looking for a way to efficiently retrieve these values in a tuple.

Validation schema docs here:
https://streamlink.github.io/api/validate.html#streamlink.plugin.api.validate._schemas.UnionGetSchema

The initial commit already turned the array's objects into tuples, so the result could be destructed/unpacked later on:
- [`5e7bd53` (#5731)](https://github.com/streamlink/streamlink/pull/5731/commits/5e7bd5340193794bade9d3b2a96d9564b956f0b7#diff-76fd9e08fdbc41636192f68bc043a8cd8a2c0032ade82288f97483b46d116c55R75-R88)
- [`5e7bd53` (#5731)](https://github.com/streamlink/streamlink/pull/5731/commits/5e7bd5340193794bade9d3b2a96d9564b956f0b7#diff-76fd9e08fdbc41636192f68bc043a8cd8a2c0032ade82288f97483b46d116c55R136)
challi1337: I'm getting this error when trying to get the stream url with the plugin and streamlink 6.6.2

![image](https://github.com/streamlink/streamlink/assets/76409412/483e7e75-3dff-41b4-9f5b-eef4b7331d32)

is this a result of it being early days of the plugin and is there some kind of workaround to still get the m3u8 url from it?


bastimeyer: Muxed stream outputs can't be represented as a single URL, hence this error message when trying to use `--stream-url`.

And btw, the current state of the plugin with its refresh-expiration logic on top of the basic plugin implementation won't be merged, as I've said multiple times now.
fml09: I misunderstood what you meant at first, Thanks for your understanding @bastimeyer. I'll add the refresh-expiration logic after this PR is got to merge. 
challi1337: > They changed more than just v1 to v2. With v2 they introduced 19+ streams (like AfreecaTV). To watch these streams, authentication is required. The name and birthday of the authenticated account has to be verified by Naver once: https://help.naver.com/service/5640/contents/20897?lang=en
> 
> 19+ streams won't return the content.livePlaybackJson part in the json when [https://api.chzzk.naver.com/service/v2/channels/{channel_id}/live-detail](https://api.chzzk.naver.com/service/v2/channels/%7Bchannel_id%7D/live-detail) is called. If authentication is required, content.adult is set to true in the response.
> 
> It seems GET requests to [https://api.chzzk.naver.com/service/v2/channels/{channel_id}/live-detail](https://api.chzzk.naver.com/service/v2/channels/%7Bchannel_id%7D/live-detail) require a cookie to return content.livePlaybackJson for 19+ streams.
> 
> In order to watch such streams, pass the parameter `--http-header Cookie='NID_SES=xxx; NID_AUT=xxx'` to streamlink. Without it, an error will be shown:
> 
> ```
> [cli][info] Found matching plugin chzzk for URL https://chzzk.naver.com/live/1ad5aa0f6c6741b072528fad5e5e76b1
> [cli][error] Unable to validate response text: ValidationError(AnySchema):
>   ValidationError(dict):
>     Unable to validate value of key 'message'
>     Context(type):
>       Type of None should be str, but is NoneType
>   ValidationError(dict):
>     Unable to validate value of key 'livePlaybackJson'
>     Context(type):
>       Type of None should be str, but is NoneType
> ```

This seemed to work fine until recently and now the error comes up even with the cookie values. Anyone experiencing same issue?

```
C:\Users\Administrator>streamlink --loglevel=all https://chzzk.naver.com/live/c40ce04da08cbe2ffa0422210c5480de
[12:51:49.743644][cli][debug] OS:         Windows 11
[12:51:49.744690][cli][debug] Python:     3.12.2
[12:51:49.744690][cli][debug] OpenSSL:    OpenSSL 3.0.13 30 Jan 2024
[12:51:49.744690][cli][debug] Streamlink: 6.7.0
[12:51:49.745691][cli][debug] Dependencies:
[12:51:49.746745][cli][debug]  certifi: 2024.2.2
[12:51:49.747746][cli][debug]  isodate: 0.6.1
[12:51:49.747746][cli][debug]  lxml: 5.1.0
[12:51:49.748821][cli][debug]  pycountry: 23.12.11
[12:51:49.749832][cli][debug]  pycryptodome: 3.20.0
[12:51:49.749832][cli][debug]  PySocks: 1.7.1
[12:51:49.749832][cli][debug]  requests: 2.31.0
[12:51:49.751334][cli][debug]  trio: 0.24.0
[12:51:49.752350][cli][debug]  trio-websocket: 0.11.1
[12:51:49.753355][cli][debug]  typing-extensions: 4.10.0
[12:51:49.754370][cli][debug]  urllib3: 2.2.1
[12:51:49.754370][cli][debug]  websocket-client: 1.7.0
[12:51:49.754370][cli][debug] Arguments:
[12:51:49.755373][cli][debug]  url=https://chzzk.naver.com/live/c40ce04da08cbe2ffa0422210c5480de
[12:51:49.755373][cli][debug]  --loglevel=all
[12:51:49.755373][cli][debug]  --player=E:\mpv\mpv.exe
[12:51:49.755373][cli][debug]  --default-stream=['best']
[12:51:49.755373][cli][debug]  --ringbuffer-size=629145600
[12:51:49.756876][cli][debug]  --stream-segment-attempts=30
[12:51:49.756876][cli][debug]  --stream-segment-threads=10
[12:51:49.756876][cli][debug]  --stream-segment-timeout=20.0
[12:51:49.756876][cli][debug]  --stream-timeout=50.0
[12:51:49.757901][cli][debug]  --hls-live-edge=10
[12:51:49.758087][cli][debug]  --ffmpeg-ffmpeg=C:\Program Files\Streamlink\ffmpeg\ffmpeg.exe
[12:51:49.758087][cli][debug]  --http-header=[('Cookie', "'NID_SES=CookiePart1Replaced; NID_AUT=CookiePart2Replaced'")]
[12:51:49.758087][cli][info] Found matching plugin chzzk for URL https://chzzk.naver.com/live/c40ce04da08cbe2ffa0422210c5480de
error: Unable to validate response text: ValidationError(AnySchema):
  ValidationError(dict):
    Unable to validate value of key 'message'
    Context(type):
      Type of None should be str, but is NoneType
  ValidationError(dict):
    Unable to validate value of key 'livePlaybackJson'
    Context(type):
      Type of None should be str, but is NoneType
```
mjaustro: It seems like Chzzk is out of beta now. Is the plugin ready for merging? 
bastimeyer: This PR will require a few modifications before it can get merged. I will have a look later today when I get back home and then update it myself.

Is there an official statement somewhere that the site went out of its beta phase?
fml09: @bastimeyer  
https://game.naver.com/lounge/chzzk/board/detail/4033868
mjaustro: > @bastimeyer https://game.naver.com/lounge/chzzk/board/detail/4033868

Thanks! Here is also an easier to translate post: https://game.naver.com/lounge/chzzk/board/detail/4033764
bastimeyer: I've force-pushed onto your PR-branch, @fml09, which btw is `master`, so be aware of that when creating follow-up PRs.

The plugin should be fine now unless I've missed something. If you want to check and validate other streams, please go ahead. Then we can merge. Thanks.

```
$ ./script/test-plugin-urls.py chzzk -r CHANNEL_ID 3497a9a7221cc3ee5d3f95991d9f95e9 -r VIDEO_ID 1459675
:: Finding streams for URL: https://chzzk.naver.com/live/3497a9a7221cc3ee5d3f95991d9f95e9
:: Found streams: 144p, 360p, 480p, 720p, 1080p, worst, best
:: Finding streams for URL: https://chzzk.naver.com/video/1459675
:: Found streams: 144p, 720p, 1080p, worst, best
```

```
$ streamlink -l debug https://chzzk.naver.com/live/3497a9a7221cc3ee5d3f95991d9f95e9 best
[cli][debug] OS:         Linux-6.8.6-1-git-x86_64-with-glibc2.39
[cli][debug] Python:     3.12.3
[cli][debug] OpenSSL:    OpenSSL 3.3.0 9 Apr 2024
[cli][debug] Streamlink: 6.7.3+20.gda1eb135
[cli][debug] Dependencies:
[cli][debug]  certifi: 2024.2.2
[cli][debug]  exceptiongroup: 1.2.1
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 5.2.1
[cli][debug]  pycountry: 23.12.11
[cli][debug]  pycryptodome: 3.20.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.31.0
[cli][debug]  trio: 0.25.0
[cli][debug]  trio-websocket: 0.11.1
[cli][debug]  typing-extensions: 4.11.0
[cli][debug]  urllib3: 2.2.1
[cli][debug]  websocket-client: 1.8.0
[cli][debug] Arguments:
[cli][debug]  url=https://chzzk.naver.com/live/3497a9a7221cc3ee5d3f95991d9f95e9
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --player=/usr/bin/mpv
[cli][info] Found matching plugin chzzk for URL https://chzzk.naver.com/live/3497a9a7221cc3ee5d3f95991d9f95e9
[utils.l10n][debug] Language code: en_US
[cli][info] Available streams: 144p (worst), 360p, 480p, 720p, 1080p (best)
[cli][info] Opening stream: 1080p (hls)
[cli][info] Starting player: /usr/bin/mpv
[stream.hls][debug] Reloading playlist
[cli][debug] Pre-buffering 8192 bytes
[stream.hls][debug] First Sequence: 8561; Last Sequence: 8563
[stream.hls][debug] Start offset: 0; Duration: None; Start Sequence: 8561; End Sequence: None
[stream.hls][debug] Adding segment 8561 to queue
[stream.hls][debug] Adding segment 8562 to queue
[stream.hls][debug] Adding segment 8563 to queue
[stream.hls][debug] Writing segment 8561 to output
[stream.hls][debug] Segment initialization 8561 complete
[cli.output][debug] Opening subprocess: ['/usr/bin/mpv', '--force-media-title=https://chzzk.naver.com/live/3497a9a7221cc3ee5d3f95991d9f95e9', '-']
[cli][debug] Writing stream to output
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 8564 to queue
[stream.hls][debug] Writing segment 8561 to output
[stream.hls][debug] Segment 8561 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 8565 to queue
[stream.hls][debug] Writing segment 8562 to output
[stream.hls][debug] Segment 8562 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 8566 to queue
[cli][info] Player closed
[stream.segmented][debug] Closing worker thread
[stream.segmented][debug] Closing writer thread
[cli][info] Stream ended
[cli][info] Closing currently open stream...
```

```
$ streamlink -l debug https://chzzk.naver.com/video/1459675 best
[cli][debug] OS:         Linux-6.8.6-1-git-x86_64-with-glibc2.39
[cli][debug] Python:     3.12.3
[cli][debug] OpenSSL:    OpenSSL 3.3.0 9 Apr 2024
[cli][debug] Streamlink: 6.6.1+10.g01c9f2d4.dirty
[cli][debug] Dependencies:
[cli][debug]  certifi: 2024.2.2
[cli][debug]  exceptiongroup: 1.2.1
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 5.2.1
[cli][debug]  pycountry: 23.12.11
[cli][debug]  pycryptodome: 3.20.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.31.0
[cli][debug]  trio: 0.25.0
[cli][debug]  trio-websocket: 0.11.1
[cli][debug]  typing-extensions: 4.11.0
[cli][debug]  urllib3: 2.2.1
[cli][debug]  websocket-client: 1.8.0
[cli][debug] Arguments:
[cli][debug]  url=https://chzzk.naver.com/video/1459675
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --player=/usr/bin/mpv
[cli][info] Found matching plugin chzzk for URL https://chzzk.naver.com/video/1459675
[utils.l10n][debug] Language code: en_US
[stream.dash][debug] Available languages for DASH audio streams: NONE (using: n/a)
[cli][info] Available streams: 144p (worst), 720p, 1080p (best)
[cli][info] Opening stream: 1080p (dash)
[cli][info] Starting player: /usr/bin/mpv
[stream.dash][debug] Opening DASH reader for: ('CB11586B7C4E93033D58DED0B06BBC892E14', None, '7535b304-0d9b-11ef-85c1-a0369ffdb798') - video/mp2t
[stream.dash.manifest][debug] Generating segment timeline for static playlist: ('CB11586B7C4E93033D58DED0B06BBC892E14', None, '7535b304-0d9b-11ef-85c1-a0369ffdb798')
[cli][debug] Pre-buffering 8192 bytes
[stream.dash][debug] video/mp2t segment 0: downloading (1970-01-01T00:00:00.000000Z / 2024-05-09T19:12:23.654517Z)
[stream.dash][debug] video/mp2t segment 1: downloading (1970-01-01T00:00:00.000000Z / 2024-05-09T19:12:27.441747Z)
[stream.dash][debug] video/mp2t segment 0: completed
[cli.output][debug] Opening subprocess: ['/usr/bin/mpv', '--force-media-title=https://chzzk.naver.com/video/1459675', '-']
[cli][debug] Writing stream to output
[stream.dash][debug] video/mp2t segment 2: downloading (1970-01-01T00:00:00.000000Z / 2024-05-09T19:12:28.048568Z)
[stream.dash][debug] video/mp2t segment 1: completed
[stream.dash][debug] video/mp2t segment 3: downloading (1970-01-01T00:00:00.000000Z / 2024-05-09T19:12:28.657800Z)
[stream.dash][debug] video/mp2t segment 2: completed
[stream.dash][debug] video/mp2t segment 4: downloading (1970-01-01T00:00:00.000000Z / 2024-05-09T19:12:29.264426Z)
[stream.dash][debug] video/mp2t segment 3: completed
[stream.dash][debug] video/mp2t segment 5: downloading (1970-01-01T00:00:00.000000Z / 2024-05-09T19:12:29.881816Z)
[stream.dash][debug] video/mp2t segment 4: completed
[stream.dash][debug] video/mp2t segment 6: downloading (1970-01-01T00:00:00.000000Z / 2024-05-09T19:12:30.501731Z)
[stream.dash][debug] video/mp2t segment 5: completed
[stream.dash][debug] video/mp2t segment 7: downloading (1970-01-01T00:00:00.000000Z / 2024-05-09T19:12:31.122724Z)
[stream.dash][debug] video/mp2t segment 6: completed
[stream.dash][debug] video/mp2t segment 8: downloading (1970-01-01T00:00:00.000000Z / 2024-05-09T19:12:31.737786Z)
[stream.dash][debug] video/mp2t segment 7: completed
[stream.dash][debug] video/mp2t segment 9: downloading (1970-01-01T00:00:00.000000Z / 2024-05-09T19:12:32.360098Z)
[stream.dash][debug] video/mp2t segment 8: completed
[stream.dash][debug] video/mp2t segment 10: downloading (1970-01-01T00:00:00.000000Z / 2024-05-09T19:12:32.973177Z)
[stream.dash][debug] video/mp2t segment 9: completed
[stream.dash][debug] video/mp2t segment 11: downloading (1970-01-01T00:00:00.000000Z / 2024-05-09T19:12:33.585996Z)
[stream.dash][debug] video/mp2t segment 10: completed
[stream.dash][debug] video/mp2t segment 12: downloading (1970-01-01T00:00:00.000000Z / 2024-05-09T19:12:34.191818Z)
[stream.dash][debug] video/mp2t segment 11: completed
[cli][info] Player closed
[stream.segmented][debug] Closing worker thread
[stream.segmented][debug] Closing writer thread
[cli][info] Stream ended
[cli][info] Closing currently open stream...
```

Patch:
diff --git a/docs/developing.rst b/docs/developing.rst
index c74a42a8..2fb1f580 100644
--- a/docs/developing.rst
+++ b/docs/developing.rst
@@ -105,7 +105,7 @@ performing these checks locally avoids unnecessary build failures.
     pytest path/to/test-file.py::TestClassName::test_method_name ...
 
     # check code for linting errors
-    ruff .
+    ruff check
     # check code for typing errors
     mypy
 
diff --git a/docs/install.rst b/docs/install.rst
index 378934ef..923dfccc 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -448,7 +448,8 @@ Streamlink defines a `build system <pyproject.toml_>`__ according to `PEP-517`_
       - Used for loading the CA bundle extracted from the Mozilla Included CA Certificate List
     * - runtime
       - `exceptiongroup`_
-      - Used for ``ExceptionGroup`` handling, to allow writing compatible code on all supported Python versions
+      - Only required when ``python_version<"3.11"`` |br|
+        Used for ``ExceptionGroup`` handling
     * - runtime
       - `isodate`_
       - Used for parsing ISO8601 strings
diff --git a/pyproject.toml b/pyproject.toml
index ecedb505..eeb3218e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -56,7 +56,7 @@ dynamic = [
 requires-python = ">=3.8"
 dependencies = [
   "certifi",
-  "exceptiongroup",
+  "exceptiongroup ; python_version<'3.11'",
   "isodate",
   "lxml >=4.6.4,<6",
   "pycountry",
diff --git a/src/streamlink/compat.py b/src/streamlink/compat.py
index 4fee3aa8..4b589cf0 100644
--- a/src/streamlink/compat.py
+++ b/src/streamlink/compat.py
@@ -5,8 +5,12 @@ import sys
 import warnings
 from typing import Any, Callable, Dict, Optional, Tuple
 
-# import exceptiongroup, so it can monkeypatch ExceptionGroup logic on <=py311
-import exceptiongroup  # noqa: F401
+
+try:
+    from builtins import BaseExceptionGroup, ExceptionGroup  # type: ignore[attr-defined]
+except ImportError:  # pragma: no cover
+    from exceptiongroup import BaseExceptionGroup, ExceptionGroup  # type: ignore[import]
+
 
 from streamlink.exceptions import StreamlinkDeprecationWarning
 
@@ -72,6 +76,8 @@ def deprecated(items: Dict[str, Tuple[Optional[str], Any, Any]]) -> None:
 
 
 __all__ = [
+    "BaseExceptionGroup",
+    "ExceptionGroup",
     "deprecated",
     "detect_encoding",
     "is_darwin",
diff --git a/src/streamlink/plugins/chzzk.py b/src/streamlink/plugins/chzzk.py
new file mode 100644
index 00000000..dc8d3099
--- /dev/null
+++ b/src/streamlink/plugins/chzzk.py
@@ -0,0 +1,196 @@
+"""
+$description South Korean live-streaming platform for gaming, entertainment, and other creative content. Owned by Naver.
+$url chzzk.naver.com
+$type live, vod
+"""
+
+import logging
+import re
+
+from streamlink.plugin import Plugin, pluginmatcher
+from streamlink.plugin.api import validate
+from streamlink.stream.dash import DASHStream
+from streamlink.stream.hls import HLSStream
+
+
+log = logging.getLogger(__name__)
+
+
+class ChzzkAPI:
+    _CHANNELS_LIVE_DETAIL_URL = "https://api.chzzk.naver.com/service/v2/channels/{channel_id}/live-detail"
+    _VIDEOS_URL = "https://api.chzzk.naver.com/service/v2/videos/{video_id}"
+
+    def __init__(self, session):
+        self._session = session
+
+    def _query_api(self, url, *schemas):
+        return self._session.http.get(
+            url,
+            acceptable_status=(200, 404),
+            schema=validate.Schema(
+                validate.parse_json(),
+                validate.any(
+                    validate.all(
+                        {
+                            "code": int,
+                            "message": str,
+                        },
+                        validate.transform(lambda data: ("error", data["message"])),
+                    ),
+                    validate.all(
+                        {
+                            "code": 200,
+                            "content": dict,
+                        },
+                        validate.get("content"),
+                        *schemas,
+                        validate.transform(lambda data: ("success", data)),
+                    ),
+                ),
+            ),
+        )
+
+    def get_live_detail(self, channel_id):
+        return self._query_api(
+            self._CHANNELS_LIVE_DETAIL_URL.format(channel_id=channel_id),
+            {
+                "status": str,
+                "liveId": int,
+                "liveTitle": validate.any(str, None),
+                "liveCategory": validate.any(str, None),
+                "adult": bool,
+                "channel": validate.all(
+                    {"channelName": str},
+                    validate.get("channelName"),
+                ),
+                "livePlaybackJson": validate.none_or_all(
+                    str,
+                    validate.parse_json(),
+                    {
+                        "media": [
+                            validate.all(
+                                {
+                                    "mediaId": str,
+                                    "protocol": str,
+                                    "path": validate.url(),
+                                },
+                                validate.union_get(
+                                    "mediaId",
+                                    "protocol",
+                                    "path",
+                                ),
+                            ),
+                        ],
+                    },
+                    validate.get("media"),
+                ),
+            },
+            validate.union_get(
+                "livePlaybackJson",
+                "status",
+                "liveId",
+                "channel",
+                "liveCategory",
+                "liveTitle",
+                "adult",
+            ),
+        )
+
+    def get_videos(self, video_id):
+        return self._query_api(
+            self._VIDEOS_URL.format(video_id=video_id),
+            {
+                "inKey": validate.any(str, None),
+                "videoId": validate.any(str, None),
+                "videoTitle": validate.any(str, None),
+                "videoCategory": validate.any(str, None),
+                "adult": bool,
+                "channel": validate.all(
+                    {"channelName": str},
+                    validate.get("channelName"),
+                ),
+            },
+            validate.union_get(
+                "inKey",
+                "videoId",
+                "channel",
+                "videoCategory",
+                "videoTitle",
+                "adult",
+            ),
+        )
+
+
+@pluginmatcher(
+    name="live",
+    pattern=re.compile(
+        r"https?://chzzk\.naver\.com/live/(?P<channel_id>[^/?]+)",
+    ),
+)
+@pluginmatcher(
+    name="video",
+    pattern=re.compile(
+        r"https?://chzzk\.naver\.com/video/(?P<video_id>[^/?]+)",
+    ),
+)
+class Chzzk(Plugin):
+    _API_VOD_PLAYBACK_URL = "https://apis.naver.com/neonplayer/vodplay/v2/playback/{video_id}?key={in_key}"
+
+    _STATUS_OPEN = "OPEN"
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self._api = ChzzkAPI(self.session)
+
+    def _get_live(self, channel_id):
+        datatype, data = self._api.get_live_detail(channel_id)
+        if datatype == "error":
+            log.error(data)
+            return
+
+        media, status, self.id, self.author, self.category, self.title, adult = data
+        if status != self._STATUS_OPEN:
+            log.error("The stream is unavailable")
+            return
+        if media is None:
+            log.error(f"This stream is for {'adults only' if adult else 'unavailable'}")
+            return
+
+        for media_id, media_protocol, media_path in media:
+            if media_protocol == "HLS" and media_id == "HLS":
+                return HLSStream.parse_variant_playlist(
+                    self.session,
+                    media_path,
+                    channel_id=channel_id,
+                    ffmpeg_options={"copyts": True},
+                )
+
+    def _get_video(self, video_id):
+        datatype, data = self._api.get_videos(video_id)
+        if datatype == "error":
+            log.error(data)
+            return
+
+        self.id = video_id
+        in_key, vod_id, self.author, self.category, self.title, adult = data
+
+        if in_key is None or vod_id is None:
+            log.error(f"This stream is for {'adults only' if adult else 'unavailable'}")
+            return
+
+        for name, stream in DASHStream.parse_manifest(
+            self.session,
+            self._API_VOD_PLAYBACK_URL.format(video_id=vod_id, in_key=in_key),
+            headers={"Accept": "application/dash+xml"},
+        ).items():
+            if stream.video_representation.mimeType == "video/mp2t":
+                yield name, stream
+
+    def _get_streams(self):
+        if self.matches["live"]:
+            return self._get_live(self.match["channel_id"])
+        elif self.matches["video"]:
+            return self._get_video(self.match["video_id"])
+
+
+__plugin__ = Chzzk
diff --git a/src/streamlink/plugins/twitch.py b/src/streamlink/plugins/twitch.py
index e0bc23dd..32666d04 100644
--- a/src/streamlink/plugins/twitch.py
+++ b/src/streamlink/plugins/twitch.py
@@ -541,7 +541,7 @@ class TwitchClientIntegrity:
         headers: Mapping[str, str],
         device_id: str,
     ) -> Optional[Tuple[str, int]]:
-        from exceptiongroup import BaseExceptionGroup  # noqa: PLC0415, I001
+        from streamlink.compat import BaseExceptionGroup  # noqa: PLC0415
         from streamlink.webbrowser.cdp import CDPClient, CDPClientSession, devtools  # noqa: PLC0415
 
         url = f"https://www.twitch.tv/{channel}"
diff --git a/src/streamlink/webbrowser/webbrowser.py b/src/streamlink/webbrowser/webbrowser.py
index f64be09f..2df2d02f 100644
--- a/src/streamlink/webbrowser/webbrowser.py
+++ b/src/streamlink/webbrowser/webbrowser.py
@@ -8,8 +8,8 @@ from subprocess import DEVNULL
 from typing import AsyncContextManager, AsyncGenerator, Generator, List, Optional, Union
 
 import trio
-from exceptiongroup import BaseExceptionGroup
 
+from streamlink.compat import BaseExceptionGroup
 from streamlink.utils.path import resolve_executable
 from streamlink.webbrowser.exceptions import WebbrowserError
 


Test patch:
diff --git a/tests/plugins/test_chzzk.py b/tests/plugins/test_chzzk.py
new file mode 100644
index 00000000..19701da8
--- /dev/null
+++ b/tests/plugins/test_chzzk.py
@@ -0,0 +1,11 @@
+from streamlink.plugins.chzzk import Chzzk
+from tests.plugins import PluginCanHandleUrl
+
+
+class TestPluginCanHandleUrlChzzk(PluginCanHandleUrl):
+    __plugin__ = Chzzk
+
+    should_match_groups = [
+        (("live", "https://chzzk.naver.com/live/CHANNEL_ID"), {"channel_id": "CHANNEL_ID"}),
+        (("video", "https://chzzk.naver.com/video/VIDEO_ID"), {"video_id": "VIDEO_ID"}),
+    ]
diff --git a/tests/webbrowser/cdp/test_client.py b/tests/webbrowser/cdp/test_client.py
index 40210a7f..1c63dc6c 100644
--- a/tests/webbrowser/cdp/test_client.py
+++ b/tests/webbrowser/cdp/test_client.py
@@ -4,9 +4,9 @@ from unittest.mock import ANY, AsyncMock, Mock, call
 
 import pytest
 import trio
-from exceptiongroup import ExceptionGroup
 from trio.testing import wait_all_tasks_blocked
 
+from streamlink.compat import ExceptionGroup
 from streamlink.session import Streamlink
 from streamlink.webbrowser.cdp.client import CDPClient, CDPClientSession, RequestPausedHandler
 from streamlink.webbrowser.cdp.connection import CDPConnection, CDPSession
diff --git a/tests/webbrowser/cdp/test_connection.py b/tests/webbrowser/cdp/test_connection.py
index 397ccdd0..4ad8a5b7 100644
--- a/tests/webbrowser/cdp/test_connection.py
+++ b/tests/webbrowser/cdp/test_connection.py
@@ -7,10 +7,10 @@ from unittest.mock import AsyncMock
 
 import pytest
 import trio
-from exceptiongroup import ExceptionGroup
 from trio.testing import MockClock, wait_all_tasks_blocked
 from trio_websocket import CloseReason, ConnectionClosed, ConnectionTimeout  # type: ignore[import]
 
+from streamlink.compat import ExceptionGroup
 from streamlink.webbrowser.cdp.connection import CDPConnection, CDPEventListener, CDPSession
 from streamlink.webbrowser.cdp.devtools.target import SessionID, TargetID
 from streamlink.webbrowser.cdp.devtools.util import T_JSON_DICT
diff --git a/tests/webbrowser/test_webbrowser.py b/tests/webbrowser/test_webbrowser.py
index 05770c5e..a56d1ca4 100644
--- a/tests/webbrowser/test_webbrowser.py
+++ b/tests/webbrowser/test_webbrowser.py
@@ -7,9 +7,8 @@ from typing import List, Optional
 
 import pytest
 import trio
-from exceptiongroup import BaseExceptionGroup
 
-from streamlink.compat import is_win32
+from streamlink.compat import BaseExceptionGroup, is_win32
 from streamlink.webbrowser.exceptions import WebbrowserError
 from streamlink.webbrowser.webbrowser import Webbrowser