Repository: streamlink/streamlink

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...
```



Discussion:
As you can see in the diff of 67b9fca3b52b6e5651d42b58ad3177d15618a823, what I did in order to fix the exception from being raised when writing the content to Streamlink's RingBuffer, was turning the `OPCODE_TEXT` responses from the websocket stream into `bytes`, because it looked like twitcasting was accidentally sending the wrong opcode (#4604).

From what it looks like though, they are sending two separate message types on the same websocket connection for some reason: binary stream data, and auxiliary text JSON data.

Could you please apply the following diff which basically discards everything that's not `OPCODE_BINARY` or `OPCODE_CONT`, and see if that fixes the issue?

```diff
diff --git a/src/streamlink/plugins/twitcasting.py b/src/streamlink/plugins/twitcasting.py
index e2f8fb09..206845d4 100644
--- a/src/streamlink/plugins/twitcasting.py
+++ b/src/streamlink/plugins/twitcasting.py
@@ -104,7 +104,7 @@ class TwitCastingWsClient(WebsocketClient):
 
     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)
```

Resolving patch:
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_close(self, *args, **kwargs):
 
     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)


Test patch: