Repository: streamlink/streamlink

Issue:
Audio Spanish
I don't hear this video in Spanish, why?,  Is there any way I can keep it from being heard in English?
```
https://www.atresplayer.com/antena3/series/el-cuento-de-la-criada/temporada-1/capitulo-6-el-lugar-de-la-mujer_5b364bee7ed1a8dd360b5b7b/
```



### Logs

```
"C:\Program Files (x86)\Streamlink\bin\streamlink.exe" h
ttps://www.atresplayer.com/antena3/series/el-cuento-de-la-criada/temporada-1/cap
itulo-6-el-lugar-de-la-mujer_5b364bee7ed1a8dd360b5b7b/ best --player ffplay -l d
ebug
[cli][debug] OS:         Windows 7
[cli][debug] Python:     3.5.2
[cli][debug] Streamlink: 0.14.2
[cli][debug] Requests(2.19.1), Socks(1.6.7), Websocket(0.48.0)
[cli][info] Found matching plugin atresplayer for URL https://www.atresplayer.co
m/antena3/series/el-cuento-de-la-criada/temporada-1/capitulo-6-el-lugar-de-la-mu
jer_5b364bee7ed1a8dd360b5b7b/
[plugin.atresplayer][debug] API URL: https://api.atresplayer.com/client/v1/playe
r/episode/5b364bee7ed1a8dd360b5b7b
[plugin.atresplayer][debug] Stream source: https://geodeswowa3player.akamaized.n
et/vcg/_definst_/assets4/2018/06/29/7875368D-BF45-4012-9EBC-C4F78984B672/hls.smi
l/playlist.m3u8?pulse=assets4%2F2018%2F06%2F29%2F7875368D-BF45-4012-9EBC-C4F7898
4B672%2F%7C1531195580%7Cb6e79f7291966a25070a79138cd19cb9 (application/vnd.apple.
mpegurl)
[stream.hls][debug] Using external audio tracks for stream 1080p (language=es, n
ame=Spanish)
[stream.hls][debug] Using external audio tracks for stream 720p (language=es, na
me=Spanish)
[stream.hls][debug] Using external audio tracks for stream 480p (language=es, na
me=Spanish)
[stream.hls][debug] Using external audio tracks for stream 360p (language=es, na
me=Spanish)
[stream.hls][debug] Using external audio tracks for stream 240p (language=es, na
me=Spanish)
[plugin.atresplayer][debug] Stream source: https://geodeswowa3player.akamaized.n
et/vcg/_definst_/assets4/2018/06/29/7875368D-BF45-4012-9EBC-C4F78984B672/dash.sm
il/manifest_mvlist.mpd (application/dash+xml)
[cli][info] Available streams: 240p (worst), 240p+a128k, 360p, 360p+a128k, 480p,
 480p+a128k, 720p, 720p+a128k, 1080p, 1080p+a128k (best)
[cli][info] Starting player: ffplay
....................
....................
....................
```

Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index 0da9dd5e..d60852f6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,9 +12,14 @@ matrix:
   - python: '3.5'
     env: BUILD_DOCS=yes BUILD_INSTALLER=yes BUILD_SDIST=yes DEPLOY_PYPI=yes
   - python: '3.6'
-  - python: '3.7-dev'
+  - python: '3.7'
+    dist: xenial
+    sudo: true
+  - python: '3.8-dev'
+    dist: xenial
+    sudo: true
   allow_failures:
-  - python: '3.7-dev'
+  - python: '3.8-dev'
 
 before_install:
   - pip install --disable-pip-version-check --upgrade pip setuptools
diff --git a/appveyor.yml b/appveyor.yml
index c40ae00f..1b26ac10 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -5,10 +5,12 @@ environment:
     - PYTHON: "C:\\Python34"
     - PYTHON: "C:\\Python35"
     - PYTHON: "C:\\Python36"
+    - PYTHON: "C:\\Python37"
     - PYTHON: "C:\\Python27-x64"
     - PYTHON: "C:\\Python34-x64"
     - PYTHON: "C:\\Python35-x64"
     - PYTHON: "C:\\Python36-x64"
+    - PYTHON: "C:\\Python37-x64"
 
 install:
   # If there is a newer build queued for the same PR, cancel this one.
diff --git a/src/streamlink/stream/dash.py b/src/streamlink/stream/dash.py
index e202e53b..1d1c5b65 100644
--- a/src/streamlink/stream/dash.py
+++ b/src/streamlink/stream/dash.py
@@ -11,6 +11,7 @@ from streamlink.stream.stream import Stream
 from streamlink.stream.dash_manifest import MPD, sleeper, sleep_until, utc, freeze_timeline
 from streamlink.stream.ffmpegmux import FFMPEGMuxer
 from streamlink.stream.segmented import SegmentedStreamReader, SegmentedStreamWorker, SegmentedStreamWriter
+from streamlink.utils.l10n import Language
 
 log = logging.getLogger(__name__)
 
@@ -196,6 +197,31 @@ class DASHStream(Stream):
         if not audio:
             audio = [None]
 
+        locale = session.localization
+        locale_lang = locale.language
+        lang = None
+        available_languages = set()
+
+        # if the locale is explicitly set, prefer that language over others
+        for aud in audio:
+            if aud and aud.lang:
+                available_languages.add(aud.lang)
+                try:
+                    if locale.explicit and aud.lang and Language.get(aud.lang) == locale_lang:
+                        lang = aud.lang
+                except LookupError:
+                    continue
+
+        if not lang:
+            # filter by the first language that appears
+            lang = audio[0] and audio[0].lang
+
+        log.debug("Available languages for DASH audio streams: {0} (using: {1})".format(", ".join(available_languages) or "NONE", lang or "n/a"))
+
+        # if the language is given by the stream, filter out other languages that do not match
+        if len(available_languages) > 1:
+            audio = list(filter(lambda a: a.lang is None or a.lang == lang, audio))
+
         for vid, aud in itertools.product(video, audio):
             stream = DASHStream(session, mpd, vid, aud, **args)
             stream_name = []
diff --git a/src/streamlink/stream/hls.py b/src/streamlink/stream/hls.py
index 1faba0ae..0fe12ad5 100644
--- a/src/streamlink/stream/hls.py
+++ b/src/streamlink/stream/hls.py
@@ -295,15 +295,18 @@ class MuxedHLSStream(MuxedStream):
 
     def __init__(self, session, video, audio, force_restart=False, ffmpeg_options=None, **args):
         tracks = [video]
+        maps = ["0:v"]
         if audio:
             if isinstance(audio, list):
                 tracks.extend(audio)
             else:
                 tracks.append(audio)
+        for i in range(1, len(tracks)):
+            maps.append("{0}:a".format(i))
         substreams = map(lambda url: HLSStream(session, url, force_restart=force_restart, **args), tracks)
         ffmpeg_options = ffmpeg_options or {}
 
-        super(MuxedHLSStream, self).__init__(session, *substreams, format="mpegts", **ffmpeg_options)
+        super(MuxedHLSStream, self).__init__(session, *substreams, format="mpegts", maps=maps, **ffmpeg_options)
 
 
 class HLSStream(HTTPStream):
diff --git a/src/streamlink/utils/l10n.py b/src/streamlink/utils/l10n.py
index 0164c7cd..ef4eb8a1 100644
--- a/src/streamlink/utils/l10n.py
+++ b/src/streamlink/utils/l10n.py
@@ -1,4 +1,5 @@
 import locale
+import logging
 
 from streamlink.compat import is_py2
 
@@ -16,6 +17,8 @@ DEFAULT_LANGUAGE = "en"
 DEFAULT_COUNTRY = "US"
 DEFAULT_LANGUAGE_CODE = "{0}_{1}".format(DEFAULT_LANGUAGE, DEFAULT_COUNTRY)
 
+log = logging.getLogger(__name__)
+
 
 class Country(object):
     def __init__(self, alpha2, alpha3, numeric, name, official_name=None):
@@ -147,6 +150,7 @@ class Localization(object):
                 self._language_code = DEFAULT_LANGUAGE_CODE
             else:
                 raise
+        log.debug("Language code: {0}".format(self._language_code))
 
     def equivalent(self, language=None, country=None):
         equivalent = True


Test patch:
diff --git a/tests/streams/test_dash.py b/tests/streams/test_dash.py
index 807b8a18..cf406185 100644
--- a/tests/streams/test_dash.py
+++ b/tests/streams/test_dash.py
@@ -1,7 +1,4 @@
 import unittest
-import unittest
-from streamlink.stream.dash import DASHStreamWorker
-from tests.mock import MagicMock, patch, ANY, Mock, call
 
 from streamlink import PluginError
 from streamlink.stream import *
@@ -41,8 +38,8 @@ class TestDASHStream(unittest.TestCase):
             Mock(adaptationSets=[
                 Mock(contentProtection=None,
                      representations=[
-                         Mock(id=1, mimeType="audio/mp4", bandwidth=128.0),
-                         Mock(id=2, mimeType="audio/mp4", bandwidth=256.0)
+                         Mock(id=1, mimeType="audio/mp4", bandwidth=128.0, lang='en'),
+                         Mock(id=2, mimeType="audio/mp4", bandwidth=256.0, lang='en')
                      ])
             ])
         ])
@@ -63,7 +60,7 @@ class TestDASHStream(unittest.TestCase):
                      representations=[
                          Mock(id=1, mimeType="video/mp4", height=720),
                          Mock(id=2, mimeType="video/mp4", height=1080),
-                         Mock(id=3, mimeType="audio/aac", bandwidth=128.0)
+                         Mock(id=3, mimeType="audio/aac", bandwidth=128.0, lang='en')
                      ])
             ])
         ])
@@ -84,8 +81,8 @@ class TestDASHStream(unittest.TestCase):
                      representations=[
                          Mock(id=1, mimeType="video/mp4", height=720),
                          Mock(id=2, mimeType="video/mp4", height=1080),
-                         Mock(id=3, mimeType="audio/aac", bandwidth=128.0),
-                         Mock(id=4, mimeType="audio/aac", bandwidth=256.0)
+                         Mock(id=3, mimeType="audio/aac", bandwidth=128.0, lang='en'),
+                         Mock(id=4, mimeType="audio/aac", bandwidth=256.0, lang='en')
                      ])
             ])
         ])
@@ -98,6 +95,108 @@ class TestDASHStream(unittest.TestCase):
             sorted(["720p+a128k", "1080p+a128k", "720p+a256k", "1080p+a256k"])
         )
 
+    @patch('streamlink.stream.dash.MPD')
+    def test_parse_manifest_audio_multi_lang(self, mpdClass):
+        mpd = mpdClass.return_value = Mock(periods=[
+            Mock(adaptationSets=[
+                Mock(contentProtection=None,
+                     representations=[
+                         Mock(id=1, mimeType="video/mp4", height=720),
+                         Mock(id=2, mimeType="video/mp4", height=1080),
+                         Mock(id=3, mimeType="audio/aac", bandwidth=128.0, lang='en'),
+                         Mock(id=4, mimeType="audio/aac", bandwidth=128.0, lang='es')
+                     ])
+            ])
+        ])
+
+        streams = DASHStream.parse_manifest(self.session, self.test_url)
+        mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd")
+
+        self.assertSequenceEqual(
+            sorted(list(streams.keys())),
+            sorted(["720p", "1080p"])
+        )
+
+        self.assertEqual(streams["720p"].audio_representation.lang, "en")
+        self.assertEqual(streams["1080p"].audio_representation.lang, "en")
+
+    @patch('streamlink.stream.dash.MPD')
+    def test_parse_manifest_audio_multi_lang_alpha3(self, mpdClass):
+        mpd = mpdClass.return_value = Mock(periods=[
+            Mock(adaptationSets=[
+                Mock(contentProtection=None,
+                     representations=[
+                         Mock(id=1, mimeType="video/mp4", height=720),
+                         Mock(id=2, mimeType="video/mp4", height=1080),
+                         Mock(id=3, mimeType="audio/aac", bandwidth=128.0, lang='eng'),
+                         Mock(id=4, mimeType="audio/aac", bandwidth=128.0, lang='spa')
+                     ])
+            ])
+        ])
+
+        streams = DASHStream.parse_manifest(self.session, self.test_url)
+        mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd")
+
+        self.assertSequenceEqual(
+            sorted(list(streams.keys())),
+            sorted(["720p", "1080p"])
+        )
+
+        self.assertEqual(streams["720p"].audio_representation.lang, "eng")
+        self.assertEqual(streams["1080p"].audio_representation.lang, "eng")
+
+    @patch('streamlink.stream.dash.MPD')
+    def test_parse_manifest_audio_invalid_lang(self, mpdClass):
+        mpd = mpdClass.return_value = Mock(periods=[
+            Mock(adaptationSets=[
+                Mock(contentProtection=None,
+                     representations=[
+                         Mock(id=1, mimeType="video/mp4", height=720),
+                         Mock(id=2, mimeType="video/mp4", height=1080),
+                         Mock(id=3, mimeType="audio/aac", bandwidth=128.0, lang='en_no_voice'),
+                     ])
+            ])
+        ])
+
+        streams = DASHStream.parse_manifest(self.session, self.test_url)
+        mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd")
+
+        self.assertSequenceEqual(
+            sorted(list(streams.keys())),
+            sorted(["720p", "1080p"])
+        )
+
+        self.assertEqual(streams["720p"].audio_representation.lang, "en_no_voice")
+        self.assertEqual(streams["1080p"].audio_representation.lang, "en_no_voice")
+
+    @patch('streamlink.stream.dash.MPD')
+    def test_parse_manifest_audio_multi_lang_locale(self, mpdClass):
+        self.session.localization.language.alpha2 = "es"
+        self.session.localization.explicit = True
+
+        mpd = mpdClass.return_value = Mock(periods=[
+            Mock(adaptationSets=[
+                Mock(contentProtection=None,
+                     representations=[
+                         Mock(id=1, mimeType="video/mp4", height=720),
+                         Mock(id=2, mimeType="video/mp4", height=1080),
+                         Mock(id=3, mimeType="audio/aac", bandwidth=128.0, lang='en'),
+                         Mock(id=4, mimeType="audio/aac", bandwidth=128.0, lang='es')
+                     ])
+            ])
+        ])
+
+        streams = DASHStream.parse_manifest(self.session, self.test_url)
+        mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd")
+
+        self.assertSequenceEqual(
+            sorted(list(streams.keys())),
+            sorted(["720p", "1080p"])
+        )
+
+        self.assertEqual(streams["720p"].audio_representation.lang, "es")
+        self.assertEqual(streams["1080p"].audio_representation.lang, "es")
+
     @patch('streamlink.stream.dash.MPD')
     def test_parse_manifest_drm(self, mpdClass):
         mpd = mpdClass.return_value = Mock(periods=[Mock(adaptationSets=[Mock(contentProtection="DRM")])])
@@ -122,7 +221,7 @@ class TestDASHStream(unittest.TestCase):
     @patch('streamlink.stream.dash.DASHStreamReader')
     @patch('streamlink.stream.dash.FFMPEGMuxer')
     def test_stream_open_video_audio(self, muxer, reader):
-        stream = DASHStream(self.session, Mock(), Mock(id=1, mimeType="video/mp4"), Mock(id=2, mimeType="audio/mp3"))
+        stream = DASHStream(self.session, Mock(), Mock(id=1, mimeType="video/mp4"), Mock(id=2, mimeType="audio/mp3", lang='en'))
         open_reader = reader.return_value = Mock()
 
         stream.open()
@@ -202,7 +301,7 @@ class TestDASHStreamWorker(unittest.TestCase):
     @patch("streamlink.stream.dash_manifest.time.sleep")
     def test_duplicate_rep_id(self, sleep):
         representation_vid = Mock(id=1, mimeType="video/mp4", height=720)
-        representation_aud = Mock(id=1, mimeType="audio/aac")
+        representation_aud = Mock(id=1, mimeType="audio/aac", lang='en')
 
         mpd = Mock(dynamic=False,
                    publishTime=1,