back-to: you should use there api and not build all urls as a hardcode,
this would most likely cleanup the code.

---

**LIVE** from api

http://www.ruv.is/sites/all/themes/at_ruv/scripts/ruv-stream.php?channel=ruv&format=json
http://www.ruv.is/sites/all/themes/at_ruv/scripts/ruv-stream.php?channel=ruv2&format=json

http://www.ruv.is/sites/all/themes/at_ruv/scripts/ruv-stream.php?channel=ras1&format=json
http://www.ruv.is/sites/all/themes/at_ruv/scripts/ruv-stream.php?channel=ras2&format=json
http://www.ruv.is/sites/all/themes/at_ruv/scripts/ruv-stream.php?channel=ras3&format=json

**VOD** from website

http://www.ruv.is/spila/ruv/ol-2018-listhlaup-karla/20180217

```js
 theoplayer.onReady = function () {
   var video = document.createElement('video');
   video.controls = "controls";
   video.poster = "http://cdn-img.ruv.is/sites/default/files/styles/1200x750/public/kringlumyndir/26243-75-1_0.jpg?";
   video.src = "http://sip-ruv-vod.dcp.adaptive.level3.net/lokad/4953386R12.mp4.m3u8";
   video.className = "player col12";
   video.preload = "none";
   video.autoplay = "autoplay";
   video.style = "width:100%;";
   video.type = "application/x-mpegURL";

   document.getElementById("sarpur-player").appendChild(video);
   var player = theoplayer(video);
```

hannespetur: > you should use there api and not build all urls as a hardcode,
this would most likely cleanup the code.

I am not seeing a way to get the correct HLS URLs with the api you sent. Can you elaborate?

Best,
Hannes
back-to: for
http://www.ruv.is/sites/all/themes/at_ruv/scripts/ruv-stream.php?channel=ruv2&format=json
if will get

```json
{
    "result": [
      "rtmp:\/\/ruvruverllivefs.fplive.net\/ruvruverllive-live\/",
      "https:\/\/ruvruverl-live-hls.secure.footprint.net\/ID-TIME\/ruv\/ruv2erl\/index.m3u8"
    ],
    "ipinfo": ["0.0.0.0", "ABC"],
    "user_agent": ["web"]
}
```

---

`https://ruvruverl-live-hls.secure.footprint.net/...-.../ruv/ruv2erl/index.m3u8`

this is the m3u8 url

```
#EXTM3U
#EXT-X-VERSION:5
#EXT-X-STREAM-INF:BANDWIDTH=950400,CODECS="avc1.77.30,mp4a.40.2",RESOLUTION=640x360
index/stream1.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1390400,CODECS="avc1.77.30,mp4a.40.2",RESOLUTION=852x480
index/stream2.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2710400,CODECS="avc1.4d401f,mp4a.40.2",RESOLUTION=1280x720
index/stream3.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=4030400,CODECS="avc1.4d4028,mp4a.40.2",RESOLUTION=1920x1080
index/stream4.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5570400,CODECS="avc1.4d4028,mp4a.40.2",RESOLUTION=1920x1080
index/stream5.m3u8
```


```
$ streamlink "https://ruvruverl-live-hls.secure.footprint.net/...-.../ruv/ruv2erl/index.m3u8"
[cli][info] Found matching plugin hls for URL https://ruvruverl-live-hls.secure.footprint.net/...-.../ruv/ruv2erl/index.m3u8
[plugin.hls][debug] URL=https://ruvruverl-live-hls.secure.footprint.net/...-.../ruv/ruv2erl/index.m3u8; params={}
[cli][info] Available streams: 360p (worst), 480p, 720p, 1080p_alt, 1080p (best)
[cli][info] Opening stream: 1080p (hls)
[stream.hls][debug] Reloading playlist
[stream.hls][debug] First Sequence: 98252; Last Sequence: 99051
[stream.hls][debug] Start offset: 0; Duration: None; Start Sequence: 99049; End Sequence: None
[stream.hls][debug] Adding segment 99049 to queue
[cli][debug] Pre-buffering 8192 bytes
[stream.hls][debug] Adding segment 99050 to queue
[stream.hls][debug] Adding segment 99051 to queue
[stream.hls][debug] Download of segment 99049 complete
[cli][info] Starting player: /usr/bin/mpv
[cli][debug] Writing stream to output
[stream.hls][debug] Download of segment 99050 complete
[stream.hls][debug] Download of segment 99051 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 99052 to queue
[stream.hls][debug] Download of segment 99052 complete
[stream.hls][debug] Reloading playlist
[stream.hls][debug] Adding segment 99053 to queue
[stream.hls][debug] Download of segment 99053 complete
```

can't test all of them because of geo blocking but they should work aswell.
hannespetur: Amazing find. The API has a 1080p live stream which is not available at the old hard-coded URL! Thank you very much. The latest changed replace the hard-coded URLs with the URLs provided by the API.

Best,
Hannes
gravyboat: Good stuff, thanks @hannespetur and @back-to!
diff --git a/src/streamlink/plugins/ruv.py b/src/streamlink/plugins/ruv.py
index 09db4860..5873d330 100644
--- a/src/streamlink/plugins/ruv.py
+++ b/src/streamlink/plugins/ruv.py
@@ -3,40 +3,34 @@
 import re
 
 from streamlink.plugin import Plugin
-from streamlink.stream import RTMPStream, HLSStream
+from streamlink.stream import HLSStream
 
 from streamlink.plugin.api import http
+from streamlink.plugin.api import validate
 
-RTMP_LIVE_URL = "rtmp://ruv{0}livefs.fplive.net/ruv{0}live-live/stream{1}"
-RTMP_SARPURINN_URL = "rtmp://sipvodfs.fplive.net/sipvod/{0}/{1}{2}.{3}"
-
-HLS_RUV_LIVE_URL = "http://ruvruv-live.hls.adaptive.level3.net/ruv/ruv/index/stream{0}.m3u8"
-HLS_RADIO_LIVE_URL = "http://sip-live.hds.adaptive.level3.net/hls-live/ruv-{0}/_definst_/live/stream1.m3u8"
-HLS_SARPURINN_URL = "http://sip-ruv-vod.dcp.adaptive.level3.net/{0}/{1}{2}.{3}.m3u8"
+# URL to the RUV LIVE API
+RUV_LIVE_API = """http://www.ruv.is/sites/all/themes/at_ruv/scripts/\
+ruv-stream.php?channel={0}&format=json"""
 
 
 _live_url_re = re.compile(r"""^(?:https?://)?(?:www\.)?ruv\.is/
-                                (?P<channel_path>
-                                    ruv|
-                                    ras1|
-                                    ras-1|
-                                    ras2|
-                                    ras-2|
-                                    rondo
+                                (?P<stream_id>
+                                    ruv/?$|
+                                    ruv2/?$|
+                                    ruv-2/?$|
+                                    ras1/?$|
+                                    ras2/?$|
+                                    rondo/?$
                                 )
                                 /?
                                 """, re.VERBOSE)
 
-_sarpurinn_url_re = re.compile(r"""^(?:https?://)?(?:www\.)?ruv\.is/sarpurinn/
-                                    (?:
+_sarpurinn_url_re = re.compile(r"""^(?:https?://)?(?:www\.)?ruv\.is/spila/
+                                    (?P<stream_id>
                                         ruv|
                                         ruv2|
                                         ruv-2|
                                         ruv-aukaras|
-                                        ras1|
-                                        ras-1|
-                                        ras2|
-                                        ras-2
                                     )
                                     /
                                     [a-zA-Z0-9_-]+
@@ -45,37 +39,26 @@ _sarpurinn_url_re = re.compile(r"""^(?:https?://)?(?:www\.)?ruv\.is/sarpurinn/
                                     /?
                                     """, re.VERBOSE)
 
-_rtmp_url_re = re.compile(r"""rtmp://sipvodfs\.fplive.net/sipvod/
-                                (?P<status>
-                                    lokad|
-                                    opid
-                                )
-                                /
-                                (?P<date>[0-9]+/[0-9][0-9]/[0-9][0-9]/)?
-                                (?P<id>[A-Z0-9\$_]+)
-                                \.
-                                (?P<ext>
-                                    mp4|
-                                    mp3
-                                )""", re.VERBOSE)
-
-_id_map = {
-    "ruv": "ruv",
-    "ras1": "ras1",
-    "ras-1": "ras1",
-    "ras2": "ras2",
-    "ras-2": "ras2",
-    "rondo": "ras3"
-}
+_single_re = re.compile(r"""(?P<url>http://[0-9a-zA-Z\-\.]*/
+                            (lokad|opid)
+                            /
+                            ([0-9]+/[0-9][0-9]/[0-9][0-9]/)?
+                            ([A-Z0-9\$_]+\.mp4\.m3u8)
+                            )
+                         """, re.VERBOSE)
+
+_multi_re = re.compile(r"""(?P<base_url>http://[0-9a-zA-Z\-\.]*/
+                            (lokad|opid)
+                            /)
+                            manifest.m3u8\?tlm=hls&streams=
+                            (?P<streams>[0-9a-zA-Z\/\.\,:]+)
+                         """, re.VERBOSE)
 
 
 class Ruv(Plugin):
     @classmethod
     def can_handle_url(cls, url):
-        if _live_url_re.match(url):
-            return _live_url_re.match(url)
-        else:
-            return _sarpurinn_url_re.match(url)
+        return _live_url_re.match(url) or _sarpurinn_url_re.match(url)
 
     def __init__(self, url):
         Plugin.__init__(self, url)
@@ -83,75 +66,77 @@ class Ruv(Plugin):
 
         if live_match:
             self.live = True
-            self.channel_path = live_match.group("channel_path")
+            self.stream_id = live_match.group("stream_id")
+
+            # Remove slashes
+            self.stream_id.replace("/", "")
+
+            # Remove dashes
+            self.stream_id.replace("-", "")
+
+            # Rondo is identified as ras3
+            if self.stream_id == "rondo":
+                self.stream_id = "ras3"
         else:
             self.live = False
 
     def _get_live_streams(self):
-        stream_id = _id_map[self.channel_path]
+        # Get JSON API
+        res = http.get(RUV_LIVE_API.format(self.stream_id))
 
-        if stream_id == "ruv":
-            qualities_rtmp = ["720p", "480p", "360p", "240p"]
-
-            for i, quality in enumerate(qualities_rtmp):
-                yield quality, RTMPStream(
-                    self.session,
-                    {
-                        "rtmp": RTMP_LIVE_URL.format(stream_id, i + 1),
-                        "pageUrl": self.url,
-                        "live": True
-                    }
-                )
+        # Parse the JSON API
+        json_res = http.json(res)
 
-            qualities_hls = ["240p", "360p", "480p", "720p"]
-            for i, quality_hls in enumerate(qualities_hls):
-                yield quality_hls, HLSStream(
-                    self.session,
-                    HLS_RUV_LIVE_URL.format(i + 1)
-                )
+        for url in json_res["result"]:
+            if url.startswith("rtmp:"):
+                continue
 
-        else:
-            yield "audio", RTMPStream(self.session, {
-                "rtmp": RTMP_LIVE_URL.format(stream_id, 1),
-                "pageUrl": self.url,
-                "live": True
-            })
+            # Get available streams
+            streams = HLSStream.parse_variant_playlist(self.session, url)
 
-            yield "audio", HLSStream(
-                self.session,
-                HLS_RADIO_LIVE_URL.format(stream_id)
-            )
+            for quality, hls in streams.items():
+                yield quality, hls
 
     def _get_sarpurinn_streams(self):
-        res = http.get(self.url)
-        match = _rtmp_url_re.search(res.text)
-
-        if not match:
-            yield
-
-        token = match.group("id")
-        status = match.group("status")
-        extension = match.group("ext")
-        date = match.group("date")
-        if not date:
-            date = ""
+        # Get HTML page
+        res = http.get(self.url).text
+        lines = "\n".join([l for l in res.split("\n") if "video.src" in l])
+        multi_stream_match = _multi_re.search(lines)
+
+        if multi_stream_match and multi_stream_match.group("streams"):
+            base_url = multi_stream_match.group("base_url")
+            streams = multi_stream_match.group("streams").split(",")
+
+            for stream in streams:
+                if stream.count(":") != 1:
+                    continue
+
+                [token, quality] = stream.split(":")
+                quality = int(quality)
+                key = ""
+
+                if quality <= 500:
+                    key = "240p"
+                elif quality <= 800:
+                    key = "360p"
+                elif quality <= 1200:
+                    key = "480p"
+                elif quality <= 2400:
+                    key = "720p"
+                else:
+                    key = "1080p"
+
+                yield key, HLSStream(
+                    self.session,
+                    base_url + token
+                )
 
-        if extension == "mp3":
-            key = "audio"
         else:
-            key = "576p"
-
-            # HLS on Sarpurinn is currently only available on videos
-            yield key, HLSStream(
-                self.session,
-                HLS_SARPURINN_URL.format(status, date, token, extension)
-            )
-
-        yield key, RTMPStream(self.session, {
-            "rtmp": RTMP_SARPURINN_URL.format(status, date, token, extension),
-            "pageUrl": self.url,
-            "live": True
-        })
+            single_stream_match = _single_re.search(lines)
+
+            if single_stream_match:
+                url = single_stream_match.group("url")
+                yield "576p", HLSStream(self.session, url)
 
     def _get_streams(self):
         if self.live:

RUV Iceland : plugin partially outdated
### Checklist

- [x] This is a bug report.
- [ ] This is a plugin request.
- [ ] This is a feature request.
- [ ] I used the search function to find already opened/closed issues or pull requests.

### Description

RUV is Icelandic broadcasting corporation consisting of 3 radio and TV channels : 

1. Radio channels
------------------

- RAS 1 : `http://ruv.is/nolayout/popup/ras1`
- RAS 2 : `http://ruv.is/nolayout/popup/ras2`
- Rondo : `http://ruv.is/nolayout/popup/rondo`

2. TV
-----

- RUV : `http://ruv.is/ruv`
- RUV 2 : `http://ruv.is/ruv-2`
- Krakkaruv : `http://krakkaruv.is/hlusta/spila`

### Expected / Actual behavior

Channels use HLS for broacasting. In the past they used rtmp too, not sure if it's still the case. 

### Reproduction steps / Stream URLs to test

Radio
------

`streamlink -l debug "http://ruv.is/nolayout/popup/ras1"
error: No plugin can handle URL: http://ruv.is/nolayout/popup/ras1`

TV
---

```
streamlink -l debug --http-proxy "82.221.48.137:8080" "http://ruv.is/ruv" best
[cli][info] Found matching plugin ruv for URL http://ruv.is/ruv
[cli][info] Available streams: 720p_hls, 480p_hls, 240p_hls, 360p_hls, 240p (wor
st), 360p, 480p, 720p (best)
[cli][info] Opening stream: 720p (rtmp)
[stream.rtmp][debug] Spawning command: C:\Users\Ddr\Downloads\streamlink\\rtmpdu
mp\rtmpdump.exe --flv - --live --pageUrl http://ruv.is/ruv --rtmp rtmp://ruvruvl
ivefs.fplive.net/ruvruvlive-live/stream1
[cli][error] Could not open stream: Error while executing subprocess
```

### Environment details (operating system, python version, etc.)

W7 PRO/streamlink portable

### Comments, logs, screenshots, etc.

TV channels are geolocked unlike radio. 

diff --git a/tests/test_plugin_ruv.py b/tests/test_plugin_ruv.py
new file mode 100644
index 00000000..fc7d5194
--- /dev/null
+++ b/tests/test_plugin_ruv.py
@@ -0,0 +1,28 @@
+import unittest
+
+from streamlink.plugins.ruv import Ruv
+
+
+class TestPluginRuv(unittest.TestCase):
+    def test_can_handle_url(self):
+        # should match
+        self.assertTrue(Ruv.can_handle_url("ruv.is/ruv"))
+        self.assertTrue(Ruv.can_handle_url("http://ruv.is/ruv"))
+        self.assertTrue(Ruv.can_handle_url("http://ruv.is/ruv/"))
+        self.assertTrue(Ruv.can_handle_url("https://ruv.is/ruv/"))
+        self.assertTrue(Ruv.can_handle_url("http://www.ruv.is/ruv"))
+        self.assertTrue(Ruv.can_handle_url("http://www.ruv.is/ruv/"))
+        self.assertTrue(Ruv.can_handle_url("ruv.is/ruv2"))
+        self.assertTrue(Ruv.can_handle_url("ruv.is/ras1"))
+        self.assertTrue(Ruv.can_handle_url("ruv.is/ras2"))
+        self.assertTrue(Ruv.can_handle_url("ruv.is/rondo"))
+        self.assertTrue(Ruv.can_handle_url("http://www.ruv.is/spila/ruv/ol-2018-ishokki-karla/20180217"))
+        self.assertTrue(Ruv.can_handle_url("http://www.ruv.is/spila/ruv/frettir/20180217"))
+
+        # shouldn't match
+        self.assertFalse(Ruv.can_handle_url("rruv.is/ruv"))
+        self.assertFalse(Ruv.can_handle_url("ruv.is/ruvnew"))
+        self.assertFalse(Ruv.can_handle_url("https://www.bloomberg.com/live/"))
+        self.assertFalse(Ruv.can_handle_url("https://www.bloomberg.com/politics/articles/2017-04-17/french-race-up-for-grabs-days-before-voters-cast-first-ballots"))
+        self.assertFalse(Ruv.can_handle_url("http://www.tvcatchup.com/"))
+        self.assertFalse(Ruv.can_handle_url("http://www.youtube.com/"))

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov codecov coverage mock requests-mock
apt-get update apt-get install -y gcc
3.9
dev-requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi==2025.1.31
charset-normalizer==3.4.1
codecov==2.1.13
coverage==7.8.0
distlib==0.3.9
exceptiongroup==1.2.2
idna==3.10
iniconfig==2.1.0
iso-639==0.4.5
iso3166==2.1.1
Jinja2==3.1.6
MarkupSafe==3.0.2
mock==5.2.0
packaging==24.2
pluggy==1.5.0
pycryptodome==3.22.0
pynsist==2.8
PySocks==1.7.1
pytest==8.3.5
pytest-cov==6.0.0
requests==2.32.3
requests-mock==1.12.1
requests_download==0.1.2
-e git+https://github.com/streamlink/streamlink.git@db9f6640df5bf2cdb8d2acdf2960fe1fc96acfec#egg=streamlink
tomli==2.2.1
urllib3==2.3.0
websocket-client==1.8.0
yarg==0.1.10

name: streamlink
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - codecov==2.1.13
      - coverage==7.8.0
      - distlib==0.3.9
      - exceptiongroup==1.2.2
      - idna==3.10
      - iniconfig==2.1.0
      - iso-639==0.4.5
      - iso3166==2.1.1
      - jinja2==3.1.6
      - markupsafe==3.0.2
      - mock==5.2.0
      - packaging==24.2
      - pluggy==1.5.0
      - pycryptodome==3.22.0
      - pynsist==2.8
      - pysocks==1.7.1
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - requests==2.32.3
      - requests-download==0.1.2
      - requests-mock==1.12.1
      - tomli==2.2.1
      - urllib3==2.3.0
      - websocket-client==1.8.0
      - yarg==0.1.10
prefix: /opt/conda/envs/streamlink

tests/test_plugin_ruv.py::TestPluginRuv::test_can_handle_url
src/streamlink/plugins/ruv.py
src/streamlink/plugins/ruv.py