Repository: streamlink/streamlink

Issue:
cli: --player="" --player-passthrough=hls causes AttributeError: 'NoneType' object has no attribute 'name'
### Checklist

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

### Collaboration

- [X] [I will provide feedback should a pull request be opened with a fix for the problem](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#pull-request-feedback)

### Streamlink version

[cli][info] Your Streamlink version (6.10.0) is up to date!

### Description

Whenever i use --player-passthrough=hls it gives nonetype error while http work fine but does not provide all the information to vlc player such as total time and seekablity.
I am using streamlink on termux android and using passthrough with twitch as well as with m3u8 links. 

### Debug log

```text
"https://worker.vidlink.pro/catflix?url=https://s.ezwatch.xyz/stream_6602d21d8a416/uwu.m3u8" best --player-passthrough=hls --loglevel=debug
[session][debug] Loading plugin: hls
[cli][debug] OS:         Linux-4.19.191+-armv8l-with-libc
[cli][debug] Python:     3.11.10
[cli][debug] OpenSSL:    OpenSSL 3.3.2 3 Sep 2024
[cli][debug] Streamlink: 6.10.0
[cli][debug] Dependencies:
[cli][debug]  certifi: 2024.8.30
[cli][debug]  isodate: 0.6.1
[cli][debug]  lxml: 5.3.0
[cli][debug]  pycountry: 24.6.1
[cli][debug]  pycryptodome: 3.20.0
[cli][debug]  PySocks: 1.7.1
[cli][debug]  requests: 2.32.3
[cli][debug]  trio: 0.26.2
[cli][debug]  trio-websocket: 0.11.1
[cli][debug]  typing-extensions: 4.12.2
[cli][debug]  urllib3: 2.2.3
[cli][debug]  websocket-client: 1.8.0
[cli][debug] Arguments:
[cli][debug]  url=https://worker.vidlink.pro/catflix?url=https://s.ezwatch.xyz/stream_6602d21d8a416/uwu.m3u8
[cli][debug]  stream=['best']
[cli][debug]  --loglevel=debug
[cli][debug]  --player-continuous-http=True
[cli][debug]  --player-external-http=True
[cli][debug]  --player-external-http-interface=127.0.0.1
[cli][debug]  --player-external-http-port=4567
[cli][debug]  --player-passthrough=['hls']
[cli][info] Found matching plugin hls for URL https://worker.vidlink.pro/catflix?url=https://s.ezwatch.xyz/stream_6602d21d8a416/uwu.m3u8
[plugins.hls][debug] URL=https://worker.vidlink.pro/catflix?url=https://s.ezwatch.xyz/stream_6602d21d8a416/uwu.m3u8; params={}
[utils.l10n][debug] Language code: en_US
[cli][info] Available streams: live (worst, best)
[cli][info] Opening stream: live (hls)
Traceback (most recent call last):
  File "/data/data/com.termux/files/usr/bin/streamlink", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/data/data/com.termux/files/usr/lib/python3.11/site-packages/streamlink_cli/main.py", line 990, in main
    exit_code = run(parser)
                ^^^^^^^^^^^
  File "/data/data/com.termux/files/usr/lib/python3.11/site-packages/streamlink_cli/main.py", line 957, in run
    handle_url()
  File "/data/data/com.termux/files/usr/lib/python3.11/site-packages/streamlink_cli/main.py", line 564, in handle_url
    handle_stream(plugin, streams, stream_name)
  File "/data/data/com.termux/files/usr/lib/python3.11/site-packages/streamlink_cli/main.py", line 422, in handle_stream
    success = output_stream_passthrough(stream, formatter)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/data/data/com.termux/files/usr/lib/python3.11/site-packages/streamlink_cli/main.py", line 279, in output_stream_passthrough
    output = PlayerOutput(
             ^^^^^^^^^^^^^
  File "/data/data/com.termux/files/usr/lib/python3.11/site-packages/streamlink_cli/output/player.py", line 189, in __init__
    self.playerargs = self.playerargsfactory(
                      ^^^^^^^^^^^^^^^^^^^^^^^
  File "/data/data/com.termux/files/usr/lib/python3.11/site-packages/streamlink_cli/output/player.py", line 212, in playerargsfactory
    executable = path.name
                 ^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'name'
```



Discussion:
> ```
> [cli][debug]  --player-continuous-http=True
> [cli][debug]  --player-external-http=True
> [cli][debug]  --player-external-http-interface=127.0.0.1
> [cli][debug]  --player-external-http-port=4567
> [cli][debug]  --player-passthrough=['hls']
> ```

Combining these options doesn't make any sense because `--player-passthrough` will take precedence. Both `--player-continuous-http` and `--player-external-http` will be ignored in this case. Even if you don't include `--player-passthrough`, the remaining options won't make any sense, because `--player-external-http` will take precedence over `--player-continuous-http`. Please see the documentation before setting random launch arguments without understanding what they do.

Inputting a direct HLS playlist URL and then using `--player-passthrough=hls` also doesn't make any sense, because the same input URL will be passed to the player process as launch argument. This is what passthrough means. It doesn't use Streamlink's own HLS client implementation or any of its features at all. It's just a launch wrapper in this case. The idea is to let other plugins resolve stream URLs. The `hls` plugin in your case is just a dummy plugin which allows HLS playlist URL inputs when not using the passthrough option.

The reason why you're seeing this `AttributeError: 'NoneType' object has no attribute 'name'` error is that you don't have a `--player` value set. The default value of that argument is dynamically chosen. On initialization, it looks up Streamlink's default player (VLC) in the `PATH` env var paths (and known default paths on Windows and macOS). Since you're in a termux environment, its value is `None` because it couldn't find the `vlc` executable in the `PATH` env var paths.

The `PlayerOutput` implementation currently doesn't handle this case of the `--player` value being `None`. This needs to be fixed.

However, as said, your input arguments in combination with the input URL don't make any sense, so you simply discovered an unhandled edge case.



Patch:
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
index 44ab0964db2..d3dfeabc6ca 100644
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -292,6 +292,12 @@ def output_stream_passthrough(stream, formatter: Formatter):
     except TypeError:
         raise StreamlinkCLIError("The stream specified cannot be translated to a URL") from None
 
+    if not args.player:
+        raise StreamlinkCLIError(
+            "The default player (VLC) does not seem to be installed."
+            + " You must specify the path to a player executable with --player.",
+        )
+
     output = PlayerOutput(
         path=args.player,
         args=args.player_args,


Test patch:
diff --git a/tests/cli/main/test_output_stream_passthrough.py b/tests/cli/main/test_output_stream_passthrough.py
new file mode 100644
index 00000000000..9c7a03be156
--- /dev/null
+++ b/tests/cli/main/test_output_stream_passthrough.py
@@ -0,0 +1,31 @@
+import re
+from pathlib import Path
+
+import pytest
+
+import streamlink_cli.main
+import tests
+from streamlink.session import Streamlink
+
+
+@pytest.fixture(autouse=True)
+def session(session: Streamlink):
+    session.plugins.load_path(Path(tests.__path__[0]) / "plugin")
+
+    return session
+
+
+@pytest.mark.parametrize(
+    "argv",
+    [pytest.param(["--player-passthrough=hls", "http://test.se", "hls"], id="no-player")],
+    indirect=["argv"],
+)
+def test_no_player(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], argv: list):
+    monkeypatch.setattr("streamlink_cli.argparser.find_default_player", lambda *_, **__: None)
+
+    with pytest.raises(SystemExit) as exc_info:
+        streamlink_cli.main.main()
+    assert exc_info.value.code == 1
+
+    out, _err = capsys.readouterr()
+    assert re.search(r"error: The default player \(\w+\) does not seem to be installed\.", out)