Repository: streamlink/streamlink

Issue:
Specify manually the network interface to use in a multi-homed setup?
## Feature Request
Add the possibility to manually specify the network interface to use. Which can be very useful for multi-homed networks with multiple network interfaces.

### Description

In Streamlink you can specify the proxy to use for streaming. Similar to that it would be also very useful if it would be possible to specify which network interface to use for streaming.

### Expected / Actual behavior

Be able to specify for example "--interface eth1" to use the eth1 interface for streaming.

[Love Streamlink? Please consider supporting our collective. Thanks!](https://opencollective.com/streamlink/donate)



Discussion:
https://github.com/requests/toolbelt/blob/master/requests_toolbelt/adapters/source.py
might be useful for this feature
closing,
this feature is not really important for Streamlink.

Resolving patch:
diff --git a/src/streamlink/session.py b/src/streamlink/session.py
--- a/src/streamlink/session.py
+++ b/src/streamlink/session.py
@@ -2,8 +2,11 @@
 import pkgutil
 from collections import OrderedDict
 from functools import lru_cache
+from socket import AF_INET, AF_INET6
 
 import requests
+import requests.packages.urllib3.util.connection as urllib3_connection
+from requests.packages.urllib3.util.connection import allowed_gai_family
 
 from streamlink import __version__, plugins
 from streamlink.compat import is_win32
@@ -30,6 +33,9 @@ class Streamlink:
     def __init__(self, options=None):
         self.http = api.HTTPSession()
         self.options = Options({
+            "interface": None,
+            "ipv4": False,
+            "ipv6": False,
             "hds-live-edge": 10.0,
             "hds-segment-attempts": 3,
             "hds-segment-threads": 1,
@@ -83,7 +89,13 @@ def set_option(self, key, value):
         **Available options**:
 
         ======================== =========================================
-        hds-live-edge            ( float) Specify the time live HDS
+        interface                (str) Set the network interface,
+                                 default: ``None``
+        ipv4                     (bool) Resolve address names to IPv4 only.
+                                 This option overrides ipv6, default: ``False``
+        ipv6                     (bool) Resolve address names to IPv6 only.
+                                 This option overrides ipv4, default: ``False``
+        hds-live-edge            (float) Specify the time live HDS
                                  streams will start from the edge of
                                  stream, default: ``10.0``
 
@@ -242,7 +254,29 @@ def set_option(self, key, value):
 
         """
 
-        if key == "http-proxy":
+        if key == "interface":
+            for scheme, adapter in self.http.adapters.items():
+                if scheme not in ("http://", "https://"):
+                    continue
+                if not value:
+                    adapter.poolmanager.connection_pool_kw.pop("source_address")
+                else:
+                    adapter.poolmanager.connection_pool_kw.update(
+                        # https://docs.python.org/3/library/socket.html#socket.create_connection
+                        source_address=(value, 0)
+                    )
+            self.options.set(key, None if not value else value)
+
+        elif key == "ipv4" or key == "ipv6":
+            self.options.set(key, value)
+            if value:
+                self.options.set("ipv6" if key == "ipv4" else "ipv4", False)
+                urllib3_connection.allowed_gai_family = \
+                    (lambda: AF_INET) if key == "ipv4" else (lambda: AF_INET6)
+            else:
+                urllib3_connection.allowed_gai_family = allowed_gai_family
+
+        elif key == "http-proxy":
             self.http.proxies["http"] = update_scheme("http://", value)
             if "https" not in self.http.proxies:
                 self.http.proxies["https"] = update_scheme("http://", value)
diff --git a/src/streamlink_cli/argparser.py b/src/streamlink_cli/argparser.py
--- a/src/streamlink_cli/argparser.py
+++ b/src/streamlink_cli/argparser.py
@@ -303,6 +303,28 @@ def build_parser():
         Default is system locale.
         """
     )
+    general.add_argument(
+        "--interface",
+        type=str,
+        metavar="INTERFACE",
+        help="""
+        Set the network interface.
+        """
+    )
+    general.add_argument(
+        "-4", "--ipv4",
+        action="store_true",
+        help="""
+        Resolve address names to IPv4 only. This option overrides :option:`-6`.
+        """
+    )
+    general.add_argument(
+        "-6", "--ipv6",
+        action="store_true",
+        help="""
+        Resolve address names to IPv6 only. This option overrides :option:`-4`.
+        """
+    )
 
     player = parser.add_argument_group("Player options")
     player.add_argument(
diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py
--- a/src/streamlink_cli/main.py
+++ b/src/streamlink_cli/main.py
@@ -751,6 +751,15 @@ def setup_streamlink():
 
 def setup_options():
     """Sets Streamlink options."""
+    if args.interface:
+        streamlink.set_option("interface", args.interface)
+
+    if args.ipv4:
+        streamlink.set_option("ipv4", args.ipv4)
+
+    if args.ipv6:
+        streamlink.set_option("ipv6", args.ipv6)
+
     if args.hls_live_edge:
         streamlink.set_option("hls-live-edge", args.hls_live_edge)
 


Test patch:
diff --git a/tests/test_session.py b/tests/test_session.py
--- a/tests/test_session.py
+++ b/tests/test_session.py
@@ -1,6 +1,9 @@
 import os
 import unittest
-from unittest.mock import call, patch
+from socket import AF_INET, AF_INET6
+from unittest.mock import Mock, call, patch
+
+from requests.packages.urllib3.util.connection import allowed_gai_family
 
 from streamlink import NoPluginError, Streamlink
 from streamlink.plugin.plugin import HIGH_PRIORITY, LOW_PRIORITY
@@ -192,6 +195,66 @@ def test_set_and_get_locale(self):
         self.assertEqual(session.localization.language.alpha2, "en")
         self.assertEqual(session.localization.language_code, "en_US")
 
+    @patch("streamlink.session.api")
+    def test_interface(self, mock_api):
+        adapter_http = Mock(poolmanager=Mock(connection_pool_kw={}))
+        adapter_https = Mock(poolmanager=Mock(connection_pool_kw={}))
+        adapter_foo = Mock(poolmanager=Mock(connection_pool_kw={}))
+        mock_api.HTTPSession.return_value = Mock(adapters={
+            "http://": adapter_http,
+            "https://": adapter_https,
+            "foo://": adapter_foo
+        })
+        session = self.subject(load_plugins=False)
+        self.assertEqual(session.get_option("interface"), None)
+
+        session.set_option("interface", "my-interface")
+        self.assertEqual(adapter_http.poolmanager.connection_pool_kw, {"source_address": ("my-interface", 0)})
+        self.assertEqual(adapter_https.poolmanager.connection_pool_kw, {"source_address": ("my-interface", 0)})
+        self.assertEqual(adapter_foo.poolmanager.connection_pool_kw, {})
+        self.assertEqual(session.get_option("interface"), "my-interface")
+
+        session.set_option("interface", None)
+        self.assertEqual(adapter_http.poolmanager.connection_pool_kw, {})
+        self.assertEqual(adapter_https.poolmanager.connection_pool_kw, {})
+        self.assertEqual(adapter_foo.poolmanager.connection_pool_kw, {})
+        self.assertEqual(session.get_option("interface"), None)
+
+    @patch("streamlink.session.urllib3_connection", allowed_gai_family=allowed_gai_family)
+    def test_ipv4_ipv6(self, mock_urllib3_connection):
+        session = self.subject(load_plugins=False)
+        self.assertEqual(session.get_option("ipv4"), False)
+        self.assertEqual(session.get_option("ipv6"), False)
+        self.assertEqual(mock_urllib3_connection.allowed_gai_family, allowed_gai_family)
+
+        session.set_option("ipv4", True)
+        self.assertEqual(session.get_option("ipv4"), True)
+        self.assertEqual(session.get_option("ipv6"), False)
+        self.assertNotEqual(mock_urllib3_connection.allowed_gai_family, allowed_gai_family)
+        self.assertEqual(mock_urllib3_connection.allowed_gai_family(), AF_INET)
+
+        session.set_option("ipv4", False)
+        self.assertEqual(session.get_option("ipv4"), False)
+        self.assertEqual(session.get_option("ipv6"), False)
+        self.assertEqual(mock_urllib3_connection.allowed_gai_family, allowed_gai_family)
+
+        session.set_option("ipv6", True)
+        self.assertEqual(session.get_option("ipv4"), False)
+        self.assertEqual(session.get_option("ipv6"), True)
+        self.assertNotEqual(mock_urllib3_connection.allowed_gai_family, allowed_gai_family)
+        self.assertEqual(mock_urllib3_connection.allowed_gai_family(), AF_INET6)
+
+        session.set_option("ipv6", False)
+        self.assertEqual(session.get_option("ipv4"), False)
+        self.assertEqual(session.get_option("ipv6"), False)
+        self.assertEqual(mock_urllib3_connection.allowed_gai_family, allowed_gai_family)
+
+        session.set_option("ipv4", True)
+        session.set_option("ipv6", False)
+        self.assertEqual(session.get_option("ipv4"), True)
+        self.assertEqual(session.get_option("ipv6"), False)
+        self.assertEqual(mock_urllib3_connection.allowed_gai_family, allowed_gai_family)
+
     def test_https_proxy_default(self):
         session = self.subject(load_plugins=False)
         session.set_option("http-proxy", "http://testproxy.com")