We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
plugins.twitcasting: Writes JSON into video files when it shouldn't
### Checklist

- [X] This is a plugin issue and not a different kind of issue
- [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 issues](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+issue%22)
- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

Latest build from the master branch

### Description

https://github.com/streamlink/streamlink/pull/4608 introduced a bug of JSON being written to the output file.

-  When running streamlink on a channel that is live but only for members, using `-o out.mp4` flag to output to a file, it creates a video file containing just a single JSON line in it:

    ```
    $ cat out.mp4
    {"type":"status","code":403,"text":"Access Forbidden"}
    ```

    The expected behavior is that it doesn't create the file in such situation, like it used to behave before https://github.com/streamlink/streamlink/pull/4608 fixes were made.

- It also adds `{"type":"status","code":504,"text":"End of Live"}` at the end of video files when the stream ends:

    ```
    $ xxd -s -128 -c 16 out.ts
    24b5bee9: 5c75 7cc6 7e38 e099 55d9 6257 59d8 eb6e  \u|.~8..U.bWY..n
    24b5bef9: b7aa 49bb ef3a dd18 7767 8c77 7dc6 6ade  ..I..:..wg.w}.j.
    24b5bf09: 6d54 2175 2acf 0926 400f 0449 2bc6 a816  mT!u*..&@..I+...
    24b5bf19: 3523 72e9 db4d 6c5a 5aba ec75 3c0a ad72  5#r..MlZZ..u<..r
    24b5bf29: 2258 0b2f ebc2 b50a 7ed3 bbbd 8d30 c77b  "X./....~....0.{
    24b5bf39: 2274 7970 6522 3a22 7374 6174 7573 222c  "type":"status",
    24b5bf49: 2263 6f64 6522 3a35 3034 2c22 7465 7874  "code":504,"text
    24b5bf59: 223a 2245 6e64 206f 6620 4c69 7665 227d  ":"End of Live"}
    ```
    ![streamlink_json](https://user-images.githubusercontent.com/1855294/175794392-7bbaa204-60ac-4170-962b-3d6dac0be9ae.png)


- Perhaps it shouldn't be writing any `response['type'] == 'status'` to the file?

- While at it, maybe there is something else that it's writing to a video file that it shouldn't? As mentioned in https://github.com/streamlink/streamlink/issues/4604#issuecomment-1166177130, Twitcasting also sends `{"type":"event","code":100,"text":""}` sometimes. Would that get written into the video file too? Is that something that should be written into it?

### Debug log

```text
[cli][debug] OS:         Linux-5.10.0-14-amd64-x86_64-with-glibc2.31
[cli][debug] Python:     3.9.2
[cli][debug] Streamlink: 4.1.0+37.g2c564dbe
[cli][debug] Dependencies:
[cli][debug]  isodate: 0.6.0
[cli][debug]  lxml: 4.7.1
[cli][debug]  pycountry: 20.7.3
[cli][debug]  pycryptodome: 3.10.1
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.28.0
[cli][debug]  websocket-client: 1.2.3
[cli][debug] Arguments:
[cli][debug]  url=https://twitcasting.tv/[REDACTED]
[cli][debug]  stream=['best']
[cli][debug]  --config=['../config']
[cli][debug]  --loglevel=debug
[cli][debug]  --output=[REDACTED]
[cli][debug]  --retry-streams=1.0
[cli][debug]  --retry-max=300
[cli][info] Found matching plugin twitcasting for URL https://twitcasting.tv/[REDACTED]
[plugins.twitcasting][debug] Live stream info: {'movie': {'id': [REDACTED], 'live': True}, 'fmp4': {'host': '202-218-171-197.twitcasting.tv', 'proto': 'wss', 'source': False, 'mobilesource': False}}
[plugins.twitcasting][debug] Real stream url: wss://202-218-171-197.twitcasting.tv/ws.app/stream/[REDACTED]/fmp4/bd/1/1500?mode=base
[cli][info] Available streams: base (worst, best)
[cli][info] Opening stream: base (stream)
[cli][info] Writing output to
[REDACTED]
[cli][debug] Checking file output
[plugin.api.websocket][debug] Connecting to: wss://202-218-171-197.twitcasting.tv/ws.app/stream/[REDACTED]/fmp4/bd/1/1500?mode=base
[cli][debug] Pre-buffering 8192 bytes
[plugin.api.websocket][debug] Connected: wss://202-218-171-197.twitcasting.tv/ws.app/stream/[REDACTED]/fmp4/bd/1/1500?mode=base
[cli][debug] Writing stream to output
[plugin.api.websocket][error] Connection to remote host was lost.
[plugin.api.websocket][debug] Closed: wss://202-218-171-197.twitcasting.tv/ws.app/stream/[REDACTED]/fmp4/bd/1/1500?mode=base
[cli][info] Stream ended
[cli][info] Closing currently open stream...
```


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/streamlink/plugins/twitcasting.py`
Content:
```
1 """
2 $description Global live broadcasting and live broadcast archiving social platform.
3 $url twitcasting.tv
4 $type live
5 """
6 
7 import hashlib
8 import logging
9 import re
10 
11 from streamlink.buffers import RingBuffer
12 from streamlink.plugin import Plugin, PluginArgument, PluginArguments, PluginError, pluginmatcher
13 from streamlink.plugin.api import validate
14 from streamlink.plugin.api.websocket import WebsocketClient
15 from streamlink.stream.stream import Stream, StreamIO
16 from streamlink.utils.url import update_qsd
17 
18 
19 log = logging.getLogger(__name__)
20 
21 
22 @pluginmatcher(re.compile(
23     r"https?://twitcasting\.tv/(?P<channel>[^/]+)"
24 ))
25 class TwitCasting(Plugin):
26     arguments = PluginArguments(
27         PluginArgument(
28             "password",
29             sensitive=True,
30             metavar="PASSWORD",
31             help="Password for private Twitcasting streams."
32         )
33     )
34     _STREAM_INFO_URL = "https://twitcasting.tv/streamserver.php?target={channel}&mode=client"
35     _STREAM_REAL_URL = "{proto}://{host}/ws.app/stream/{movie_id}/fmp4/bd/1/1500?mode={mode}"
36 
37     _STREAM_INFO_SCHEMA = validate.Schema({
38         validate.optional("movie"): {
39             "id": int,
40             "live": bool
41         },
42         validate.optional("fmp4"): {
43             "host": str,
44             "proto": str,
45             "source": bool,
46             "mobilesource": bool
47         }
48     })
49 
50     def __init__(self, url):
51         super().__init__(url)
52         self.channel = self.match.group("channel")
53 
54     def _get_streams(self):
55         stream_info = self._get_stream_info()
56         log.debug(f"Live stream info: {stream_info}")
57 
58         if not stream_info.get("movie") or not stream_info["movie"]["live"]:
59             raise PluginError("The live stream is offline")
60 
61         if not stream_info.get("fmp4"):
62             raise PluginError("Login required")
63 
64         # Keys are already validated by schema above
65         proto = stream_info["fmp4"]["proto"]
66         host = stream_info["fmp4"]["host"]
67         movie_id = stream_info["movie"]["id"]
68 
69         if stream_info["fmp4"]["source"]:
70             mode = "main"  # High quality
71         elif stream_info["fmp4"]["mobilesource"]:
72             mode = "mobilesource"  # Medium quality
73         else:
74             mode = "base"  # Low quality
75 
76         if (proto == '') or (host == '') or (not movie_id):
77             raise PluginError(f"No stream available for user {self.channel}")
78 
79         real_stream_url = self._STREAM_REAL_URL.format(proto=proto, host=host, movie_id=movie_id, mode=mode)
80 
81         password = self.options.get("password")
82         if password is not None:
83             password_hash = hashlib.md5(password.encode()).hexdigest()
84             real_stream_url = update_qsd(real_stream_url, {"word": password_hash})
85 
86         log.debug(f"Real stream url: {real_stream_url}")
87 
88         return {mode: TwitCastingStream(session=self.session, url=real_stream_url)}
89 
90     def _get_stream_info(self):
91         url = self._STREAM_INFO_URL.format(channel=self.channel)
92         res = self.session.http.get(url)
93         return self.session.http.json(res, schema=self._STREAM_INFO_SCHEMA)
94 
95 
96 class TwitCastingWsClient(WebsocketClient):
97     def __init__(self, buffer: RingBuffer, *args, **kwargs):
98         self.buffer = buffer
99         super().__init__(*args, **kwargs)
100 
101     def on_close(self, *args, **kwargs):
102         super().on_close(*args, **kwargs)
103         self.buffer.close()
104 
105     def on_data(self, wsapp, data, data_type, cont):
106         if data_type == self.OPCODE_TEXT:
107             data = bytes(data, "utf-8")
108 
109         try:
110             self.buffer.write(data)
111         except Exception as err:
112             log.error(err)
113             self.close()
114 
115 
116 class TwitCastingReader(StreamIO):
117     def __init__(self, stream: "TwitCastingStream", timeout=None):
118         super().__init__()
119         self.session = stream.session
120         self.stream = stream
121         self.timeout = timeout or self.session.options.get("stream-timeout")
122 
123         buffer_size = self.session.get_option("ringbuffer-size")
124         self.buffer = RingBuffer(buffer_size)
125 
126         self.wsclient = TwitCastingWsClient(
127             self.buffer,
128             stream.session,
129             stream.url,
130             origin="https://twitcasting.tv/"
131         )
132 
133     def open(self):
134         self.wsclient.start()
135 
136     def close(self):
137         self.wsclient.close()
138         self.buffer.close()
139 
140     def read(self, size):
141         return self.buffer.read(
142             size,
143             block=self.wsclient.is_alive(),
144             timeout=self.timeout
145         )
146 
147 
148 class TwitCastingStream(Stream):
149     def __init__(self, session, url):
150         super().__init__(session)
151         self.url = url
152 
153     def to_url(self):
154         return self.url
155 
156     def open(self):
157         reader = TwitCastingReader(self)
158         reader.open()
159         return reader
160 
161 
162 __plugin__ = TwitCasting
163
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/streamlink/plugins/twitcasting.py b/src/streamlink/plugins/twitcasting.py
--- a/src/streamlink/plugins/twitcasting.py
+++ b/src/streamlink/plugins/twitcasting.py
@@ -104,7 +104,7 @@
 
     def on_data(self, wsapp, data, data_type, cont):
         if data_type == self.OPCODE_TEXT:
-            data = bytes(data, "utf-8")
+            return
 
         try:
             self.buffer.write(data)

{"golden_diff": "diff --git a/src/streamlink/plugins/twitcasting.py b/src/streamlink/plugins/twitcasting.py\n--- a/src/streamlink/plugins/twitcasting.py\n+++ b/src/streamlink/plugins/twitcasting.py\n@@ -104,7 +104,7 @@\n \n     def on_data(self, wsapp, data, data_type, cont):\n         if data_type == self.OPCODE_TEXT:\n-            data = bytes(data, \"utf-8\")\n+            return\n \n         try:\n             self.buffer.write(data)\n", "issue": "plugins.twitcasting: Writes JSON into video files when it shouldn't\n### Checklist\r\n\r\n- [X] This is a plugin issue and not a different kind of issue\r\n- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)\r\n- [X] [I have checked the list of open and recently closed plugin issues](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+issue%22)\r\n- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)\r\n\r\n### Streamlink version\r\n\r\nLatest build from the master branch\r\n\r\n### Description\r\n\r\nhttps://github.com/streamlink/streamlink/pull/4608 introduced a bug of JSON being written to the output file.\r\n\r\n-  When running streamlink on a channel that is live but only for members, using `-o out.mp4` flag to output to a file, it creates a video file containing just a single JSON line in it:\r\n\r\n    ```\r\n    $ cat out.mp4\r\n    {\"type\":\"status\",\"code\":403,\"text\":\"Access Forbidden\"}\r\n    ```\r\n\r\n    The expected behavior is that it doesn't create the file in such situation, like it used to behave before https://github.com/streamlink/streamlink/pull/4608 fixes were made.\r\n\r\n- It also adds `{\"type\":\"status\",\"code\":504,\"text\":\"End of Live\"}` at the end of video files when the stream ends:\r\n\r\n    ```\r\n    $ xxd -s -128 -c 16 out.ts\r\n    24b5bee9: 5c75 7cc6 7e38 e099 55d9 6257 59d8 eb6e  \\u|.~8..U.bWY..n\r\n    24b5bef9: b7aa 49bb ef3a dd18 7767 8c77 7dc6 6ade  ..I..:..wg.w}.j.\r\n    24b5bf09: 6d54 2175 2acf 0926 400f 0449 2bc6 a816  mT!u*..&@..I+...\r\n    24b5bf19: 3523 72e9 db4d 6c5a 5aba ec75 3c0a ad72  5#r..MlZZ..u<..r\r\n    24b5bf29: 2258 0b2f ebc2 b50a 7ed3 bbbd 8d30 c77b  \"X./....~....0.{\r\n    24b5bf39: 2274 7970 6522 3a22 7374 6174 7573 222c  \"type\":\"status\",\r\n    24b5bf49: 2263 6f64 6522 3a35 3034 2c22 7465 7874  \"code\":504,\"text\r\n    24b5bf59: 223a 2245 6e64 206f 6620 4c69 7665 227d  \":\"End of Live\"}\r\n    ```\r\n    ![streamlink_json](https://user-images.githubusercontent.com/1855294/175794392-7bbaa204-60ac-4170-962b-3d6dac0be9ae.png)\r\n\r\n\r\n- Perhaps it shouldn't be writing any `response['type'] == 'status'` to the file?\r\n\r\n- While at it, maybe there is something else that it's writing to a video file that it shouldn't? As mentioned in https://github.com/streamlink/streamlink/issues/4604#issuecomment-1166177130, Twitcasting also sends `{\"type\":\"event\",\"code\":100,\"text\":\"\"}` sometimes. Would that get written into the video file too? Is that something that should be written into it?\r\n\r\n### Debug log\r\n\r\n```text\r\n[cli][debug] OS:         Linux-5.10.0-14-amd64-x86_64-with-glibc2.31\r\n[cli][debug] Python:     3.9.2\r\n[cli][debug] Streamlink: 4.1.0+37.g2c564dbe\r\n[cli][debug] Dependencies:\r\n[cli][debug]  isodate: 0.6.0\r\n[cli][debug]  lxml: 4.7.1\r\n[cli][debug]  pycountry: 20.7.3\r\n[cli][debug]  pycryptodome: 3.10.1\r\n[cli][debug]  PySocks: 1.7.1\r\n[cli][debug]  requests: 2.28.0\r\n[cli][debug]  websocket-client: 1.2.3\r\n[cli][debug] Arguments:\r\n[cli][debug]  url=https://twitcasting.tv/[REDACTED]\r\n[cli][debug]  stream=['best']\r\n[cli][debug]  --config=['../config']\r\n[cli][debug]  --loglevel=debug\r\n[cli][debug]  --output=[REDACTED]\r\n[cli][debug]  --retry-streams=1.0\r\n[cli][debug]  --retry-max=300\r\n[cli][info] Found matching plugin twitcasting for URL https://twitcasting.tv/[REDACTED]\r\n[plugins.twitcasting][debug] Live stream info: {'movie': {'id': [REDACTED], 'live': True}, 'fmp4': {'host': '202-218-171-197.twitcasting.tv', 'proto': 'wss', 'source': False, 'mobilesource': False}}\r\n[plugins.twitcasting][debug] Real stream url: wss://202-218-171-197.twitcasting.tv/ws.app/stream/[REDACTED]/fmp4/bd/1/1500?mode=base\r\n[cli][info] Available streams: base (worst, best)\r\n[cli][info] Opening stream: base (stream)\r\n[cli][info] Writing output to\r\n[REDACTED]\r\n[cli][debug] Checking file output\r\n[plugin.api.websocket][debug] Connecting to: wss://202-218-171-197.twitcasting.tv/ws.app/stream/[REDACTED]/fmp4/bd/1/1500?mode=base\r\n[cli][debug] Pre-buffering 8192 bytes\r\n[plugin.api.websocket][debug] Connected: wss://202-218-171-197.twitcasting.tv/ws.app/stream/[REDACTED]/fmp4/bd/1/1500?mode=base\r\n[cli][debug] Writing stream to output\r\n[plugin.api.websocket][error] Connection to remote host was lost.\r\n[plugin.api.websocket][debug] Closed: wss://202-218-171-197.twitcasting.tv/ws.app/stream/[REDACTED]/fmp4/bd/1/1500?mode=base\r\n[cli][info] Stream ended\r\n[cli][info] Closing currently open stream...\r\n```\r\n\n", "before_files": [{"content": "\"\"\"\n$description Global live broadcasting and live broadcast archiving social platform.\n$url twitcasting.tv\n$type live\n\"\"\"\n\nimport hashlib\nimport logging\nimport re\n\nfrom streamlink.buffers import RingBuffer\nfrom streamlink.plugin import Plugin, PluginArgument, PluginArguments, PluginError, pluginmatcher\nfrom streamlink.plugin.api import validate\nfrom streamlink.plugin.api.websocket import WebsocketClient\nfrom streamlink.stream.stream import Stream, StreamIO\nfrom streamlink.utils.url import update_qsd\n\n\nlog = logging.getLogger(__name__)\n\n\n@pluginmatcher(re.compile(\n    r\"https?://twitcasting\\.tv/(?P<channel>[^/]+)\"\n))\nclass TwitCasting(Plugin):\n    arguments = PluginArguments(\n        PluginArgument(\n            \"password\",\n            sensitive=True,\n            metavar=\"PASSWORD\",\n            help=\"Password for private Twitcasting streams.\"\n        )\n    )\n    _STREAM_INFO_URL = \"https://twitcasting.tv/streamserver.php?target={channel}&mode=client\"\n    _STREAM_REAL_URL = \"{proto}://{host}/ws.app/stream/{movie_id}/fmp4/bd/1/1500?mode={mode}\"\n\n    _STREAM_INFO_SCHEMA = validate.Schema({\n        validate.optional(\"movie\"): {\n            \"id\": int,\n            \"live\": bool\n        },\n        validate.optional(\"fmp4\"): {\n            \"host\": str,\n            \"proto\": str,\n            \"source\": bool,\n            \"mobilesource\": bool\n        }\n    })\n\n    def __init__(self, url):\n        super().__init__(url)\n        self.channel = self.match.group(\"channel\")\n\n    def _get_streams(self):\n        stream_info = self._get_stream_info()\n        log.debug(f\"Live stream info: {stream_info}\")\n\n        if not stream_info.get(\"movie\") or not stream_info[\"movie\"][\"live\"]:\n            raise PluginError(\"The live stream is offline\")\n\n        if not stream_info.get(\"fmp4\"):\n            raise PluginError(\"Login required\")\n\n        # Keys are already validated by schema above\n        proto = stream_info[\"fmp4\"][\"proto\"]\n        host = stream_info[\"fmp4\"][\"host\"]\n        movie_id = stream_info[\"movie\"][\"id\"]\n\n        if stream_info[\"fmp4\"][\"source\"]:\n            mode = \"main\"  # High quality\n        elif stream_info[\"fmp4\"][\"mobilesource\"]:\n            mode = \"mobilesource\"  # Medium quality\n        else:\n            mode = \"base\"  # Low quality\n\n        if (proto == '') or (host == '') or (not movie_id):\n            raise PluginError(f\"No stream available for user {self.channel}\")\n\n        real_stream_url = self._STREAM_REAL_URL.format(proto=proto, host=host, movie_id=movie_id, mode=mode)\n\n        password = self.options.get(\"password\")\n        if password is not None:\n            password_hash = hashlib.md5(password.encode()).hexdigest()\n            real_stream_url = update_qsd(real_stream_url, {\"word\": password_hash})\n\n        log.debug(f\"Real stream url: {real_stream_url}\")\n\n        return {mode: TwitCastingStream(session=self.session, url=real_stream_url)}\n\n    def _get_stream_info(self):\n        url = self._STREAM_INFO_URL.format(channel=self.channel)\n        res = self.session.http.get(url)\n        return self.session.http.json(res, schema=self._STREAM_INFO_SCHEMA)\n\n\nclass TwitCastingWsClient(WebsocketClient):\n    def __init__(self, buffer: RingBuffer, *args, **kwargs):\n        self.buffer = buffer\n        super().__init__(*args, **kwargs)\n\n    def on_close(self, *args, **kwargs):\n        super().on_close(*args, **kwargs)\n        self.buffer.close()\n\n    def on_data(self, wsapp, data, data_type, cont):\n        if data_type == self.OPCODE_TEXT:\n            data = bytes(data, \"utf-8\")\n\n        try:\n            self.buffer.write(data)\n        except Exception as err:\n            log.error(err)\n            self.close()\n\n\nclass TwitCastingReader(StreamIO):\n    def __init__(self, stream: \"TwitCastingStream\", timeout=None):\n        super().__init__()\n        self.session = stream.session\n        self.stream = stream\n        self.timeout = timeout or self.session.options.get(\"stream-timeout\")\n\n        buffer_size = self.session.get_option(\"ringbuffer-size\")\n        self.buffer = RingBuffer(buffer_size)\n\n        self.wsclient = TwitCastingWsClient(\n            self.buffer,\n            stream.session,\n            stream.url,\n            origin=\"https://twitcasting.tv/\"\n        )\n\n    def open(self):\n        self.wsclient.start()\n\n    def close(self):\n        self.wsclient.close()\n        self.buffer.close()\n\n    def read(self, size):\n        return self.buffer.read(\n            size,\n            block=self.wsclient.is_alive(),\n            timeout=self.timeout\n        )\n\n\nclass TwitCastingStream(Stream):\n    def __init__(self, session, url):\n        super().__init__(session)\n        self.url = url\n\n    def to_url(self):\n        return self.url\n\n    def open(self):\n        reader = TwitCastingReader(self)\n        reader.open()\n        return reader\n\n\n__plugin__ = TwitCasting\n", "path": "src/streamlink/plugins/twitcasting.py"}], "after_files": [{"content": "\"\"\"\n$description Global live broadcasting and live broadcast archiving social platform.\n$url twitcasting.tv\n$type live\n\"\"\"\n\nimport hashlib\nimport logging\nimport re\n\nfrom streamlink.buffers import RingBuffer\nfrom streamlink.plugin import Plugin, PluginArgument, PluginArguments, PluginError, pluginmatcher\nfrom streamlink.plugin.api import validate\nfrom streamlink.plugin.api.websocket import WebsocketClient\nfrom streamlink.stream.stream import Stream, StreamIO\nfrom streamlink.utils.url import update_qsd\n\n\nlog = logging.getLogger(__name__)\n\n\n@pluginmatcher(re.compile(\n    r\"https?://twitcasting\\.tv/(?P<channel>[^/]+)\"\n))\nclass TwitCasting(Plugin):\n    arguments = PluginArguments(\n        PluginArgument(\n            \"password\",\n            sensitive=True,\n            metavar=\"PASSWORD\",\n            help=\"Password for private Twitcasting streams.\"\n        )\n    )\n    _STREAM_INFO_URL = \"https://twitcasting.tv/streamserver.php?target={channel}&mode=client\"\n    _STREAM_REAL_URL = \"{proto}://{host}/ws.app/stream/{movie_id}/fmp4/bd/1/1500?mode={mode}\"\n\n    _STREAM_INFO_SCHEMA = validate.Schema({\n        validate.optional(\"movie\"): {\n            \"id\": int,\n            \"live\": bool\n        },\n        validate.optional(\"fmp4\"): {\n            \"host\": str,\n            \"proto\": str,\n            \"source\": bool,\n            \"mobilesource\": bool\n        }\n    })\n\n    def __init__(self, url):\n        super().__init__(url)\n        self.channel = self.match.group(\"channel\")\n\n    def _get_streams(self):\n        stream_info = self._get_stream_info()\n        log.debug(f\"Live stream info: {stream_info}\")\n\n        if not stream_info.get(\"movie\") or not stream_info[\"movie\"][\"live\"]:\n            raise PluginError(\"The live stream is offline\")\n\n        if not stream_info.get(\"fmp4\"):\n            raise PluginError(\"Login required\")\n\n        # Keys are already validated by schema above\n        proto = stream_info[\"fmp4\"][\"proto\"]\n        host = stream_info[\"fmp4\"][\"host\"]\n        movie_id = stream_info[\"movie\"][\"id\"]\n\n        if stream_info[\"fmp4\"][\"source\"]:\n            mode = \"main\"  # High quality\n        elif stream_info[\"fmp4\"][\"mobilesource\"]:\n            mode = \"mobilesource\"  # Medium quality\n        else:\n            mode = \"base\"  # Low quality\n\n        if (proto == '') or (host == '') or (not movie_id):\n            raise PluginError(f\"No stream available for user {self.channel}\")\n\n        real_stream_url = self._STREAM_REAL_URL.format(proto=proto, host=host, movie_id=movie_id, mode=mode)\n\n        password = self.options.get(\"password\")\n        if password is not None:\n            password_hash = hashlib.md5(password.encode()).hexdigest()\n            real_stream_url = update_qsd(real_stream_url, {\"word\": password_hash})\n\n        log.debug(f\"Real stream url: {real_stream_url}\")\n\n        return {mode: TwitCastingStream(session=self.session, url=real_stream_url)}\n\n    def _get_stream_info(self):\n        url = self._STREAM_INFO_URL.format(channel=self.channel)\n        res = self.session.http.get(url)\n        return self.session.http.json(res, schema=self._STREAM_INFO_SCHEMA)\n\n\nclass TwitCastingWsClient(WebsocketClient):\n    def __init__(self, buffer: RingBuffer, *args, **kwargs):\n        self.buffer = buffer\n        super().__init__(*args, **kwargs)\n\n    def on_close(self, *args, **kwargs):\n        super().on_close(*args, **kwargs)\n        self.buffer.close()\n\n    def on_data(self, wsapp, data, data_type, cont):\n        if data_type == self.OPCODE_TEXT:\n            return\n\n        try:\n            self.buffer.write(data)\n        except Exception as err:\n            log.error(err)\n            self.close()\n\n\nclass TwitCastingReader(StreamIO):\n    def __init__(self, stream: \"TwitCastingStream\", timeout=None):\n        super().__init__()\n        self.session = stream.session\n        self.stream = stream\n        self.timeout = timeout or self.session.options.get(\"stream-timeout\")\n\n        buffer_size = self.session.get_option(\"ringbuffer-size\")\n        self.buffer = RingBuffer(buffer_size)\n\n        self.wsclient = TwitCastingWsClient(\n            self.buffer,\n            stream.session,\n            stream.url,\n            origin=\"https://twitcasting.tv/\"\n        )\n\n    def open(self):\n        self.wsclient.start()\n\n    def close(self):\n        self.wsclient.close()\n        self.buffer.close()\n\n    def read(self, size):\n        return self.buffer.read(\n            size,\n            block=self.wsclient.is_alive(),\n            timeout=self.timeout\n        )\n\n\nclass TwitCastingStream(Stream):\n    def __init__(self, session, url):\n        super().__init__(session)\n        self.url = url\n\n    def to_url(self):\n        return self.url\n\n    def open(self):\n        reader = TwitCastingReader(self)\n        reader.open()\n        return reader\n\n\n__plugin__ = TwitCasting\n", "path": "src/streamlink/plugins/twitcasting.py"}]}