Repository: streamlink/streamlink

Issue:
Plugin for Niconico Live doesn't work.
<!--
Thanks for reporting a plugin issue!
USE THE TEMPLATE. Otherwise your plugin issue may be rejected.

First, see the contribution guidelines:
https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink

Also check the list of open and closed plugin issues:
https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22plugin+issue%22

Please see the text preview to avoid unnecessary formatting errors.
-->


## Plugin Issue

<!-- Replace [ ] with [x] in order to check the box -->
- [x] This is a plugin issue and I have read the contribution guidelines.


### Description

<!-- Explain the plugin issue as thoroughly as you can. -->
NicoNico Live Broadcast will not start playing and will time out.

### Reproduction steps / Explicit stream URLs to test

<!-- How can we reproduce this? Please note the exact steps below using the list format supplied. If you need more steps please add them. -->

1. install streamlink 1.4.1
2. Play any Niconico live broadcast 
    ex `$ streamlink live2.nicovideo.jp/watch/lv326495242 best`
3. Time out
　`[cli][info] Found matching plugin nicolive for URL live2.nicovideo.jp/watch/lv326495242`
    `[plugin.nicolive][error] Waiting for permit timed out.`




### Log output

<!--
TEXT LOG OUTPUT IS REQUIRED for a plugin issue!
Use the `--loglevel debug` parameter and avoid using parameters which suppress log output.
https://streamlink.github.io/cli.html#cmdoption-l

Make sure to **remove usernames and passwords**
You can copy the output to https://gist.github.com/ or paste it below.
-->

```
$ streamlink live2.nicovideo.jp/watch/lv326495242 --loglevel debug
[plugin.nicolive][debug] Restored cookies: nicolivehistory, nicosid
[cli][debug] OS:         macOS 10.15.5
[cli][debug] Python:     3.8.3
[cli][debug] Streamlink: 1.4.1
[cli][debug] Requests(2.23.0), Socks(1.7.1), Websocket(0.57.0)
[cli][info] Found matching plugin nicolive for URL live2.nicovideo.jp/watch/lv326495242
[plugin.nicolive][debug] Getting video page: http://live2.nicovideo.jp/watch/lv326495242
[plugin.nicolive][debug] Video page response code: 200
[plugin.nicolive][debug] Got wss_api_url: wss://a.live2.nicovideo.jp/wsapi/v2/watch/3645917628179?audience_token=29570850150482_3645917628179_1592250299_169bab4af6e54066e9fe092cfabf186910e48bc3
[plugin.nicolive][debug] Got broadcast_id:
[plugin.nicolive][debug] Connecting: wss://a.live2.nicovideo.jp/wsapi/v2/watch/3645917628179?audience_token=29570850150482_3645917628179_1592250299_169bab4af6e54066e9fe092cfabf186910e48bc3
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Sending: {"type": "watch", "body": {"command": "playerversion", "params": ["leo"]}}
[plugin.nicolive][debug] Sending: {"type": "watch", "body": {"command": "getpermit", "requirement": {"broadcastId": "", "route": "", "stream": {"protocol": "hls", "requireNewStream": true, "priorStreamQuality": "abr", "isLowLatency": true, "isChasePlay": false}, "room": {"isCommentable": true, "protocol": "webSocket"}}}}
[plugin.nicolive][debug] Received: {"type":"error","data":{"code":"INVALID_MESSAGE"}}
[plugin.nicolive][debug] Received: {"type":"error","data":{"code":"INVALID_MESSAGE"}}
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Received: {"type":"ping"}
[plugin.nicolive][debug] Sending: {"type": "pong", "body": {}}
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Received: {"type":"serverTime","data":{"currentMs":"2020-06-15T04:45:18.267+09:00"}}
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Received: {"type":"ping"}
[plugin.nicolive][debug] Sending: {"type": "pong", "body": {}}
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Received: {"type":"statistics","data":{"viewers":41753,"comments":16267,"adPoints":65000}}
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][debug] Received: {"type":"ping"}
[plugin.nicolive][debug] Sending: {"type": "pong", "body": {}}
[plugin.nicolive][debug] Waiting for permit...
[plugin.nicolive][error] Waiting for permit timed out.
error: No playable streams found on this URL: live2.nicovideo.jp/watch/lv326495242
```


### Additional comments, screenshots, etc.
There are reports that there was a change in the API specification in late May, but we don't know the details.



[Love Streamlink? Please consider supporting our collective. Thanks!](https://opencollective.com/streamlink/donate)



Discussion:
Hey guys,

I guess I resolved the new wss api format issue.

1. There is new parameter in wss_api_url, which is frontend_id. I leave the snippet here and example of the new wss_api_url.
```
# frontend_id from webpage
self.frontend_id = extract_text(resp.text, "&quot;frontendId&quot;:", ",&quot;")

# wss_api_url
wss://a.live2.nicovideo.jp/wsapi/v2/watch/lv326495242/timeshift?audience_token=lv326495242_anonymous-user-30352534264954_1592752486_dc0f7e344c038d2ce3e2c627bc095d72fb4a827c&frontend_id=12
```

2. There is new initial message I found as below:

```
{"type":"startWatching","data":{"stream":{"quality":"high","protocol":"hls","latency":"high","chasePlay":false},"room":{"protocol":"webSocket","commentable":true},"reconnect":false}}
```

3. The m3u8 URI is saved in type "stream" message as following below:
```
Return: 
RECEIVED: {"type":"stream","data":{"uri":"https://pa023c07d94.dmc.nico/hlsarchive/ht2_nicolive/nicolive-hamster-lv326495242_main_2c0612eb8f7799ee0bd6a083871f8c80fa5e4ddb94d870ffd747fb7d74800144/master.m3u8?ht2_nicolive=anonymous-user-30352534264954.gcsicpdhrl_qc8d18_1xah805rtldwa","syncUri":"https://pa023c07d94.dmc.nico/hlsarchive/ht2_nicolive/nicolive-hamster-lv326495242_main_2c0612eb8f7799ee0bd6a083871f8c80fa5e4ddb94d870ffd747fb7d74800144/stream_sync.json?ht2_nicolive=anonymous-user-30352534264954.gcsicpdhrl_qc8d18_1xah805rtldwa","quality":"high","availableQualities":["abr","super_high","high","normal","low","super_low"],"protocol":"hls"}}

```

I also found other repository got the same problem. Here is the reference link.
https://github.com/himananiito/livedl/issues/49

Send PR soon~

Resolving patch:
diff --git a/src/streamlink/plugins/nicolive.py b/src/streamlink/plugins/nicolive.py
--- a/src/streamlink/plugins/nicolive.py
+++ b/src/streamlink/plugins/nicolive.py
@@ -54,6 +54,8 @@ class NicoLive(Plugin):
     stream_reader = None
     _ws = None
 
+    frontend_id = None
+
     @classmethod
     def can_handle_url(cls, url):
         return _url_re.match(url) is not None
@@ -118,10 +120,20 @@ def get_wss_api_url(self):
             _log.debug(e)
             _log.warning("Failed to extract broadcast id")
 
+        try:
+            self.frontend_id = extract_text(
+                resp.text, "&quot;frontendId&quot;:", ",&quot;")
+        except Exception as e:
+            _log.debug(e)
+            _log.warning("Failed to extract frontend id")
+
+        self.wss_api_url = "{0}&frontend_id={1}".format(self.wss_api_url, self.frontend_id)
+
         _log.debug("Video page response code: {0}".format(resp.status_code))
         _log.trace(u"Video page response body: {0}".format(resp.text))
         _log.debug("Got wss_api_url: {0}".format(self.wss_api_url))
         _log.debug("Got broadcast_id: {0}".format(self.broadcast_id))
+        _log.debug("Got frontend_id: {0}".format(self.frontend_id))
 
         return self.wss_api_url.startswith("wss://")
 
@@ -172,33 +184,50 @@ def send_message(self, type_, body):
         else:
             _log.warning("wss api is not connected.")
 
-    def send_playerversion(self):
-        body = {
-            "command": "playerversion",
-            "params": ["leo"]
-        }
-        self.send_message("watch", body)
+    def send_no_body_message(self, type_):
+        msg = {"type": type_}
+        msg_json = json.dumps(msg)
+        _log.debug(u"Sending: {0}".format(msg_json))
+        if self._ws and self._ws.sock.connected:
+            self._ws.send(msg_json)
+        else:
+            _log.warning("wss api is not connected.")
 
-    def send_getpermit(self, require_new_stream=True):
+    def send_custom_message(self, msg):
+        msg_json = json.dumps(msg)
+        _log.debug(u"Sending: {0}".format(msg_json))
+        if self._ws and self._ws.sock.connected:
+            self._ws.send(msg_json)
+        else:
+            _log.warning("wss api is not connected.")
+
+    def send_playerversion(self):
         body = {
-            "command": "getpermit",
-            "requirement": {
-                "broadcastId": self.broadcast_id,
-                "route": "",
+            "type": "startWatching",
+            "data": {
                 "stream": {
+                    "quality": "high",
                     "protocol": "hls",
-                    "requireNewStream": require_new_stream,
-                    "priorStreamQuality": "abr",
-                    "isLowLatency": True,
-                    "isChasePlay": False
+                    "latency": "high",
+                    "chasePlay": False
                 },
                 "room": {
-                    "isCommentable": True,
-                    "protocol": "webSocket"
-                }
+                    "protocol": "webSocket",
+                    "commentable": True
+                },
+                "reconnect": False
             }
         }
-        self.send_message("watch", body)
+        self.send_custom_message(body)
+
+    def send_getpermit(self, require_new_stream=True):
+        body = {
+            "type": "getAkashic",
+            "data": {
+                "chasePlay": False
+            }
+        }
+        self.send_custom_message(body)
 
     def send_watching(self):
         body = {
@@ -208,12 +237,18 @@ def send_watching(self):
         self.send_message("watch", body)
 
     def send_pong(self):
-        self.send_message("pong", {})
+        self.send_no_body_message("pong")
+        self.send_no_body_message("keepSeat")
 
     def handle_api_message(self, message):
         _log.debug(u"Received: {0}".format(message))
         message_parsed = json.loads(message)
 
+        if message_parsed["type"] == "stream":
+            data = message_parsed["data"]
+            self.hls_stream_url = data["uri"]
+            self.is_stream_ready = True
+
         if message_parsed["type"] == "watch":
             body = message_parsed["body"]
             command = body["command"]


Test patch: