diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index bf357e17c1e38e3d3a166fe55845811eb3e0d776..2f4f6e9a4d90e53ff041e302d29e433403b45301 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -306,9 +306,13 @@ STATIC_ROOT = env("STATIC_ROOT", default=str(ROOT_DIR("staticfiles")))
 STATIC_URL = env("STATIC_URL", default="/staticfiles/")
 DEFAULT_FILE_STORAGE = "funkwhale_api.common.storage.ASCIIFileSystemStorage"
 
+PROXY_MEDIA = env.bool("PROXY_MEDIA", default=True)
 AWS_DEFAULT_ACL = None
-AWS_QUERYSTRING_AUTH = False
-
+AWS_QUERYSTRING_AUTH = env.bool("AWS_QUERYSTRING_AUTH", default=not PROXY_MEDIA)
+AWS_S3_MAX_MEMORY_SIZE = env.int(
+    "AWS_S3_MAX_MEMORY_SIZE", default=1000 * 1000 * 1000 * 20
+)
+AWS_QUERYSTRING_EXPIRE = env.int("AWS_QUERYSTRING_EXPIRE", default=3600)
 AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID", default=None)
 
 if AWS_ACCESS_KEY_ID:
@@ -319,6 +323,7 @@ if AWS_ACCESS_KEY_ID:
     AWS_LOCATION = env("AWS_LOCATION", default="")
     DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
 
+
 # See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS
 STATICFILES_DIRS = (str(APPS_DIR.path("static")),)
 
diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py
index 20e173d831c4bcfe8430d750c5afa1e858c0c61f..391a4b333fe45628626383c4479d4a952c9df4b5 100644
--- a/api/funkwhale_api/music/views.py
+++ b/api/funkwhale_api/music/views.py
@@ -285,7 +285,7 @@ def should_transcode(upload, format, max_bitrate=None):
     return format_need_transcoding or bitrate_need_transcoding
 
 
-def handle_serve(upload, user, format=None, max_bitrate=None):
+def handle_serve(upload, user, format=None, max_bitrate=None, proxy_media=True):
     f = upload
     # we update the accessed_date
     now = timezone.now()
@@ -329,6 +329,11 @@ def handle_serve(upload, user, format=None, max_bitrate=None):
         f = transcoded_version
         file_path = get_file_path(f.audio_file)
         mt = f.mimetype
+    if not proxy_media:
+        # we simply issue a 302 redirect to the real URL
+        response = Response(status=302)
+        response["Location"] = f.audio_file.url
+        return response
     if mt:
         response = Response(content_type=mt)
     else:
@@ -380,7 +385,11 @@ class ListenViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
         if max_bitrate:
             max_bitrate = max_bitrate * 1000
         return handle_serve(
-            upload, user=request.user, format=format, max_bitrate=max_bitrate
+            upload,
+            user=request.user,
+            format=format,
+            max_bitrate=max_bitrate,
+            proxy_media=settings.PROXY_MEDIA,
         )
 
 
diff --git a/api/funkwhale_api/subsonic/views.py b/api/funkwhale_api/subsonic/views.py
index 31bd00407b128e879392d9393f03882875c7ad49..88d7ece7c575d29bdcec0e7bbb8a056292ffd7d2 100644
--- a/api/funkwhale_api/subsonic/views.py
+++ b/api/funkwhale_api/subsonic/views.py
@@ -260,7 +260,13 @@ class SubsonicViewSet(viewsets.GenericViewSet):
         if max_bitrate:
             max_bitrate = max_bitrate * 1000
         return music_views.handle_serve(
-            upload=upload, user=request.user, format=format, max_bitrate=max_bitrate
+            upload=upload,
+            user=request.user,
+            format=format,
+            max_bitrate=max_bitrate,
+            # Subsonic clients don't expect 302 redirection unfortunately,
+            # So we have to proxy media files
+            proxy_media=True,
         )
 
     @action(detail=False, methods=["get", "post"], url_name="star", url_path="star")
diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py
index 32d95e14fc69f903f36f00cc13ef1160405f85ba..102b5a790a82d8d6fc174df72b414d0bf64033bf 100644
--- a/api/tests/music/test_views.py
+++ b/api/tests/music/test_views.py
@@ -331,7 +331,7 @@ def test_listen_correct_access(factories, logged_in_api_client):
     assert response.status_code == 200
 
 
-def test_listen_explicit_file(factories, logged_in_api_client, mocker):
+def test_listen_explicit_file(factories, logged_in_api_client, mocker, settings):
     mocked_serve = mocker.spy(views, "handle_serve")
     upload1 = factories["music.Upload"](
         library__privacy_level="everyone", import_status="finished"
@@ -344,10 +344,26 @@ def test_listen_explicit_file(factories, logged_in_api_client, mocker):
 
     assert response.status_code == 200
     mocked_serve.assert_called_once_with(
-        upload2, user=logged_in_api_client.user, format=None, max_bitrate=None
+        upload2,
+        user=logged_in_api_client.user,
+        format=None,
+        max_bitrate=None,
+        proxy_media=settings.PROXY_MEDIA,
     )
 
 
+def test_listen_no_proxy(factories, logged_in_api_client, settings):
+    settings.PROXY_MEDIA = False
+    upload = factories["music.Upload"](
+        library__privacy_level="everyone", import_status="finished"
+    )
+    url = reverse("api:v1:listen-detail", kwargs={"uuid": upload.track.uuid})
+    response = logged_in_api_client.get(url, {"upload": upload.uuid})
+
+    assert response.status_code == 302
+    assert response["Location"] == upload.audio_file.url
+
+
 @pytest.mark.parametrize(
     "mimetype,format,expected",
     [
@@ -409,7 +425,7 @@ def test_handle_serve_create_mp3_version(factories, now):
     assert response.status_code == 200
 
 
-def test_listen_transcode(factories, now, logged_in_api_client, mocker):
+def test_listen_transcode(factories, now, logged_in_api_client, mocker, settings):
     upload = factories["music.Upload"](
         import_status="finished", library__actor__user=logged_in_api_client.user
     )
@@ -420,7 +436,11 @@ def test_listen_transcode(factories, now, logged_in_api_client, mocker):
     assert response.status_code == 200
 
     handle_serve.assert_called_once_with(
-        upload, user=logged_in_api_client.user, format="mp3", max_bitrate=None
+        upload,
+        user=logged_in_api_client.user,
+        format="mp3",
+        max_bitrate=None,
+        proxy_media=settings.PROXY_MEDIA,
     )
 
 
@@ -436,7 +456,7 @@ def test_listen_transcode(factories, now, logged_in_api_client, mocker):
     ],
 )
 def test_listen_transcode_bitrate(
-    max_bitrate, expected, factories, now, logged_in_api_client, mocker
+    max_bitrate, expected, factories, now, logged_in_api_client, mocker, settings
 ):
     upload = factories["music.Upload"](
         import_status="finished", library__actor__user=logged_in_api_client.user
@@ -448,7 +468,11 @@ def test_listen_transcode_bitrate(
     assert response.status_code == 200
 
     handle_serve.assert_called_once_with(
-        upload, user=logged_in_api_client.user, format=None, max_bitrate=expected
+        upload,
+        user=logged_in_api_client.user,
+        format=None,
+        max_bitrate=expected,
+        proxy_media=settings.PROXY_MEDIA,
     )
 
 
@@ -474,7 +498,11 @@ def test_listen_transcode_in_place(
     assert response.status_code == 200
 
     handle_serve.assert_called_once_with(
-        upload, user=logged_in_api_client.user, format="mp3", max_bitrate=None
+        upload,
+        user=logged_in_api_client.user,
+        format="mp3",
+        max_bitrate=None,
+        proxy_media=settings.PROXY_MEDIA,
     )
 
 
diff --git a/api/tests/subsonic/test_views.py b/api/tests/subsonic/test_views.py
index ec61b25fa79e486e9f17d2ec2d30c0365f19389a..73f968ff47130daefa5312d1aa61e10a9a15ddde 100644
--- a/api/tests/subsonic/test_views.py
+++ b/api/tests/subsonic/test_views.py
@@ -217,7 +217,12 @@ def test_get_song(
 
 
 @pytest.mark.parametrize("f", ["json"])
-def test_stream(f, db, logged_in_api_client, factories, mocker, queryset_equal_queries):
+def test_stream(
+    f, db, logged_in_api_client, factories, mocker, queryset_equal_queries, settings
+):
+    # Even with this settings set to false, we proxy media in the subsonic API
+    # Because clients don't expect a 302 redirect
+    settings.PROXY_MEDIA = False
     url = reverse("api:subsonic-stream")
     mocked_serve = mocker.spy(music_views, "handle_serve")
     assert url.endswith("stream") is True
@@ -226,7 +231,11 @@ def test_stream(f, db, logged_in_api_client, factories, mocker, queryset_equal_q
     response = logged_in_api_client.get(url, {"f": f, "id": upload.track.pk})
 
     mocked_serve.assert_called_once_with(
-        upload=upload, user=logged_in_api_client.user, format=None, max_bitrate=None
+        upload=upload,
+        user=logged_in_api_client.user,
+        format=None,
+        max_bitrate=None,
+        proxy_media=True,
     )
     assert response.status_code == 200
     playable_by.assert_called_once_with(music_models.Track.objects.all(), None)
@@ -242,7 +251,11 @@ def test_stream_format(format, expected, logged_in_api_client, factories, mocker
     response = logged_in_api_client.get(url, {"id": upload.track.pk, "format": format})
 
     mocked_serve.assert_called_once_with(
-        upload=upload, user=logged_in_api_client.user, format=expected, max_bitrate=None
+        upload=upload,
+        user=logged_in_api_client.user,
+        format=expected,
+        max_bitrate=None,
+        proxy_media=True,
     )
     assert response.status_code == 200
 
@@ -261,7 +274,11 @@ def test_stream_bitrate(max_bitrate, expected, logged_in_api_client, factories,
     )
 
     mocked_serve.assert_called_once_with(
-        upload=upload, user=logged_in_api_client.user, format=None, max_bitrate=expected
+        upload=upload,
+        user=logged_in_api_client.user,
+        format=None,
+        max_bitrate=expected,
+        proxy_media=True,
     )
     assert response.status_code == 200
 
diff --git a/docs/admin/external-storages.rst b/docs/admin/external-storages.rst
index 73b8889046751d09712607f781911ea24d32f65d..413f8cfb2d82e105d8bcd37ad5a262eb8d15e565 100644
--- a/docs/admin/external-storages.rst
+++ b/docs/admin/external-storages.rst
@@ -55,6 +55,26 @@ by hand (which is outside the scope of this guide).
 
     At the moment, we do not support S3 when using Apache as a reverse proxy.
 
+Serving audio files directly from the bucket
+********************************************
+
+Depending on your setup, you may want to serve audio fils directly from the S3 bucket
+instead of proxying them through Funkwhale, e.g to reduce the bandwidth consumption on your server,
+or get better performance.
+
+You can achieve that by adding ``PROXY_MEDIA=false`` to your ``.env`` file.
+
+When receiving a request on the stream endpoint, Funkwhale will check for authentication and permissions,
+then issue a 302 redirect to the file URL in the bucket.
+
+This URL is actually be visible by the client, but contains a signature valid only for one hour, to ensure
+no one can reuse this URL or share it publicly to distribute unauthorized content.
+
+.. note::
+
+    Since some Subsonic clients don't support 302 redirections, Funkwhale will ignore
+    the ``PROXY_MEDIA`` setting and always proxy file when accessed through the Subsonic API.
+
 
 Securing your S3 bucket
 ***********************
diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue
index 0ad8fe7c6f61385bed9af6568677dd8158337f1b..af672876c8267efca01e398aae3c9a496e419edf 100644
--- a/front/src/components/audio/Player.vue
+++ b/front/src/components/audio/Player.vue
@@ -379,7 +379,7 @@ export default {
         },
         onunlock: function () {
           if (this.$store.state.player.playing) {
-            self.soundId = self.sound.play()
+            self.soundId = self.sound.play(self.soundId)
           }
         },
         onload: function () {
@@ -518,7 +518,8 @@ export default {
       let onlyTrack = this.$store.state.queue.tracks.length === 1
       if (this.looping === 1 || (onlyTrack && this.looping === 2)) {
         this.currentSound.seek(0)
-        this.soundId = this.currentSound.play()
+        this.$store.dispatch('player/updateProgress', 0)
+        this.soundId = this.currentSound.play(this.soundId)
       } else {
         this.$store.dispatch('player/trackEnded', this.currentTrack)
       }