diff --git a/resources/icons/ui/volume-up.png b/resources/icons/ui/volume-up.png
new file mode 100644
index 0000000000000000000000000000000000000000..4a42643f23ae3a55142a0ba0973ccfc5e33ba514
--- /dev/null
+++ b/resources/icons/ui/volume-up.png
@@ -0,0 +1,5 @@
+塒NG
+
+���
IHDR��� �������陆"���bKGD������牻��IDATH壟紫媿a鹣黗f4d黊搪,淝菷蕩伉的婒7X)"憪銰)V睷�Y事幦趏�J�嬿y籫n揠�;镥咱=}蟳烇鱵唧<瞎粗�0呏儳樌~�k壣糗Jⅵe貚Mu骘溺 �.簺%锬�4�/�9n^W�!焷�0�$��LB�o仑綅愊耐*�"�!�跜l?Rl箅!锫�葖瘁F葯榔?bGrp6炙
+筣@^�淬x葅�乲L�-虅G5Hj=Q朗獣楼恵!�'綞撲Q繠�'靭�*贋�os>�8W唨锅�3a祰�#a�澞�2�糔�7�
+_騉匋	-雦~営娠篇淇9K縸e^m�*K繰霮䎱<N�3}�,伡鱡�U^穸愾0齰`E驘S�6V賳砊夺W�!k�gse�8�.囓�?鹜蝎礕�;S忊�轛qm馽!v鴒涔M�:�bk摖|鮱]枪5覑�5$�6€穉鞛F萻k�%墨@~�蜍颚春鈰l棭M�觝J店秥D	my#謨'埠榄曫讖階f�>����IEND瓸`�
\ No newline at end of file
diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml
index 6a2c642c689803dd8839dd393d243761e7de28da..0a8587b3dc743baa06f257253545edcae9038eaa 100644
--- a/resources/qml/CommunitiesList.qml
+++ b/resources/qml/CommunitiesList.qml
@@ -56,15 +56,13 @@ Page {
             property color bubbleBackground: Nheko.colors.highlight
             property color bubbleText: Nheko.colors.highlightedText
 
-            background: Rectangle {
-                color: backgroundColor
-            }
-
             height: avatarSize + 2 * Nheko.paddingMedium
             width: ListView.view.width
             state: "normal"
             ToolTip.visible: hovered && collapsed
             ToolTip.text: model.tooltip
+            onClicked: Communities.setCurrentTagId(model.id)
+            onPressAndHold: communityContextMenu.show(model.id)
             states: [
                 State {
                     name: "highlight"
@@ -108,9 +106,6 @@ Page {
 
             }
 
-            onClicked: Communities.setCurrentTagId(model.id)
-            onPressAndHold: communityContextMenu.show(model.id)
-
             RowLayout {
                 spacing: Nheko.paddingMedium
                 anchors.fill: parent
@@ -149,6 +144,10 @@ Page {
 
             }
 
+            background: Rectangle {
+                color: backgroundColor
+            }
+
         }
 
     }
diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index eb6db29110f8ea3066a8998aa6e1f5d387cecc90..c738e5b4dae7c56892c01cb419e3e9775cdcffd4 100644
--- a/resources/qml/delegates/PlayableMediaMessage.qml
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -3,14 +3,15 @@
 // SPDX-License-Identifier: GPL-3.0-or-later
 
 import "../"
+import "../ui/media"
 import QtMultimedia 5.15
 import QtQuick 2.15
 import QtQuick.Controls 2.15
-import QtQuick.Layouts 1.2
+import QtQuick.Layouts 1.15
 import im.nheko 1.0
 
-Rectangle {
-    id: bg
+Item {
+    id: content
 
     required property double proportionalHeight
     required property int type
@@ -20,199 +21,86 @@ Rectangle {
     required property string url
     required property string body
     required property string filesize
+    property double tempWidth: Math.min(parent ? parent.width : undefined, originalWidth < 1 ? 400 : originalWidth)
+    property double tempHeight: tempWidth * proportionalHeight
+    property double divisor: isReply ? 4 : 2
+    property bool tooHigh: tempHeight > timelineRoot.height / divisor
+
+    height: (type == MtxEvent.VideoMessage ? tooHigh ? timelineRoot.height / divisor : tempHeight : 80) + fileInfoLabel.height
+    width: type == MtxEvent.VideoMessage ? tooHigh ? (timelineRoot.height / divisor) / proportionalHeight : tempWidth : 250
+
+    MxcMedia {
+        id: mxcmedia
+
+        // TODO: Show error in overlay or so?
+        onError: console.log(error)
+        roomm: room
+        // desiredVolume is a float from 0.0 -> 1.0, MediaPlayer volume is an int from 0 to 100
+        // this value automatically gets clamped for us between these two values.
+        volume: mediaControls.desiredVolume * 100
+        muted: mediaControls.muted
+    }
 
-    radius: 10
-    color: Nheko.colors.alternateBase
-    height: Math.round(content.height + 24)
-    width: parent ? parent.width : undefined
-    ListView.onPooled: height = 4
-    ListView.onReused: height = Math.round(content.height + 24)
-
-    Column {
-        id: content
-
-        width: parent.width - 24
-        anchors.centerIn: parent
-
-        Rectangle {
-            id: videoContainer
-
-            property double tempWidth: Math.min(parent ? parent.width : undefined, originalWidth < 1 ? 400 : originalWidth)
-            property double tempHeight: tempWidth * proportionalHeight
-            property double divisor: isReply ? 4 : 2
-            property bool tooHigh: tempHeight > timelineView.height / divisor
-
-            visible: type == MtxEvent.VideoMessage
-            height: tooHigh ? timelineView.height / divisor : tempHeight
-            width: tooHigh ? (timelineView.height / divisor) / proportionalHeight : tempWidth
-
-            Image {
-                anchors.fill: parent
-                source: thumbnailUrl.replace("mxc://", "image://MxcImage/")
-                asynchronous: true
-                fillMode: Image.PreserveAspectFit
+    Rectangle {
+        id: videoContainer
 
-                VideoOutput {
-                    anchors.fill: parent
-                    fillMode: VideoOutput.PreserveAspectFit
-                    flushMode: VideoOutput.FirstFrame
-                    source: mxcmedia
-                }
-
-            }
+        color: type == MtxEvent.VideoMessage ? Nheko.colors.window : "transparent"
+        width: parent.width
+        height: parent.height - fileInfoLabel.height
 
+        TapHandler {
+            onTapped: mediaControls.showControls()
         }
 
-        RowLayout {
-            width: parent.width
-
-            Text {
-                id: positionText
+        Image {
+            anchors.fill: parent
+            source: thumbnailUrl.replace("mxc://", "image://MxcImage/")
+            asynchronous: true
+            fillMode: Image.PreserveAspectFit
 
-                text: "--:--:--"
-                color: Nheko.colors.text
-            }
-
-            Slider {
-                id: progress
-
-                //indeterminate: true
-                function updatePositionTexts() {
-                    function formatTime(date) {
-                        var hh = date.getUTCHours();
-                        var mm = date.getUTCMinutes();
-                        var ss = date.getSeconds();
-                        if (hh < 10)
-                            hh = "0" + hh;
-
-                        if (mm < 10)
-                            mm = "0" + mm;
-
-                        if (ss < 10)
-                            ss = "0" + ss;
-
-                        return hh + ":" + mm + ":" + ss;
-                    }
-
-                    positionText.text = formatTime(new Date(mxcmedia.position));
-                    durationText.text = formatTime(new Date(mxcmedia.duration));
-                }
-
-                Layout.fillWidth: true
-                value: mxcmedia.position
-                from: 0
-                to: mxcmedia.duration
-                onMoved: mxcmedia.position = value
-                onValueChanged: updatePositionTexts()
-                palette: Nheko.colors
-            }
+            VideoOutput {
+                id: videoOutput
 
-            Text {
-                id: durationText
-
-                text: "--:--:--"
-                color: Nheko.colors.text
+                visible: type == MtxEvent.VideoMessage
+                clip: true
+                anchors.fill: parent
+                fillMode: VideoOutput.PreserveAspectFit
+                source: mxcmedia
+                flushMode: VideoOutput.FirstFrame
             }
 
         }
 
-        RowLayout {
-            width: parent.width
-            spacing: 15
-
-            ImageButton {
-                id: button
-
-                Layout.alignment: Qt.AlignVCenter
-                //color: Nheko.colors.window
-                //radius: 22
-                height: 32
-                width: 32
-                z: 3
-                image: ":/icons/icons/ui/arrow-pointing-down.png"
-                onClicked: {
-                    switch (button.state) {
-                    case "":
-                        mxcmedia.eventId = eventId;
-                        break;
-                    case "stopped":
-                        mxcmedia.play();
-                        console.log("play");
-                        button.state = "playing";
-                        break;
-                    case "playing":
-                        mxcmedia.pause();
-                        console.log("pause");
-                        button.state = "stopped";
-                        break;
-                    }
-                }
-                states: [
-                    State {
-                        name: "stopped"
-
-                        PropertyChanges {
-                            target: button
-                            image: ":/icons/icons/ui/play-sign.png"
-                        }
-
-                    },
-                    State {
-                        name: "playing"
-
-                        PropertyChanges {
-                            target: button
-                            image: ":/icons/icons/ui/pause-symbol.png"
-                        }
-
-                    }
-                ]
-
-                CursorShape {
-                    anchors.fill: parent
-                    cursorShape: Qt.PointingHandCursor
-                }
-
-                MxcMedia {
-                    id: mxcmedia
-
-                    roomm: room
-                    onError: console.log(errorString)
-                    onMediaStatusChanged: {
-                        if (status == MxcMedia.LoadedMedia) {
-                            progress.updatePositionTexts();
-                            button.state = "stopped";
-                        }
-                    }
-                    onStateChanged: {
-                        if (state == MxcMedia.StoppedState)
-                            button.state = "stopped";
-
-                    }
-                }
-
-            }
-
-            ColumnLayout {
-                id: col
+    }
 
-                Text {
-                    Layout.fillWidth: true
-                    text: body
-                    elide: Text.ElideRight
-                    color: Nheko.colors.text
-                }
+    MediaControls {
+        id: mediaControls
+
+        anchors.left: content.left
+        anchors.right: content.right
+        anchors.bottom: fileInfoLabel.top
+        playingVideo: type == MtxEvent.VideoMessage
+        positionValue: mxcmedia.position
+        duration: mxcmedia.duration
+        mediaLoaded: mxcmedia.loaded
+        mediaState: mxcmedia.state
+        onPositionChanged: mxcmedia.position = position
+        onPlayPauseActivated: mxcmedia.state == MediaPlayer.PlayingState ? mxcmedia.pause() : mxcmedia.play()
+        onLoadActivated: mxcmedia.eventId = eventId
+    }
 
-                Text {
-                    Layout.fillWidth: true
-                    text: filesize
-                    textFormat: Text.PlainText
-                    elide: Text.ElideRight
-                    color: Nheko.colors.text
-                }
+    // information about file name and file size
+    Label {
+        id: fileInfoLabel
 
-            }
+        anchors.bottom: content.bottom
+        text: body + " [" + filesize + "]"
+        textFormat: Text.PlainText
+        elide: Text.ElideRight
+        color: Nheko.colors.text
 
+        background: Rectangle {
+            color: Nheko.colors.base
         }
 
     }
diff --git a/resources/qml/dialogs/ReadReceipts.qml b/resources/qml/dialogs/ReadReceipts.qml
index f551bae9d3a7262bf3f81ef028293b434b9e53bf..1bfdae84984693ad6facbf064d8081b4c493fc96 100644
--- a/resources/qml/dialogs/ReadReceipts.qml
+++ b/resources/qml/dialogs/ReadReceipts.qml
@@ -66,9 +66,6 @@ ApplicationWindow {
                     hoverEnabled: true
                     ToolTip.visible: hovered
                     ToolTip.text: model.mxid
-                    background: Rectangle {
-                        color: readReceiptsRoot.color
-                    }
 
                     RowLayout {
                         id: receiptLayout
@@ -113,6 +110,10 @@ ApplicationWindow {
                         cursorShape: Qt.PointingHandCursor
                     }
 
+                    background: Rectangle {
+                        color: readReceiptsRoot.color
+                    }
+
                 }
 
             }
diff --git a/resources/qml/ui/NhekoSlider.qml b/resources/qml/ui/NhekoSlider.qml
new file mode 100644
index 0000000000000000000000000000000000000000..23e22f51b3e957f403d135c97bcbc46dfe22d41e
--- /dev/null
+++ b/resources/qml/ui/NhekoSlider.qml
@@ -0,0 +1,50 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import im.nheko 1.0
+
+Slider {
+    id: control
+
+    property color progressColor: Nheko.colors.highlight
+    property bool alwaysShowSlider: true
+    property int sliderRadius: 16
+
+    value: 0
+    implicitHeight: sliderRadius
+    padding: 0
+
+    background: Rectangle {
+        x: control.leftPadding + handle.width / 2
+        y: control.topPadding + control.availableHeight / 2 - height / 2
+        implicitWidth: 200
+        implicitHeight: control.sliderRadius / 4
+        width: control.availableWidth - handle.width
+        height: implicitHeight
+        radius: height / 2
+        color: Nheko.colors.buttonText
+
+        Rectangle {
+            width: control.visualPosition * parent.width
+            height: parent.height
+            color: control.progressColor
+            radius: 2
+        }
+
+    }
+
+    handle: Rectangle {
+        x: control.leftPadding + control.visualPosition * background.width
+        y: control.topPadding + control.availableHeight / 2 - height / 2
+        implicitWidth: control.sliderRadius
+        implicitHeight: control.sliderRadius
+        radius: control.sliderRadius / 2
+        color: control.progressColor
+        visible: Settings.mobileMode || control.alwaysShowSlider || control.hovered || control.pressed
+        border.color: control.progressColor
+    }
+
+}
diff --git a/resources/qml/ui/media/MediaControls.qml b/resources/qml/ui/media/MediaControls.qml
new file mode 100644
index 0000000000000000000000000000000000000000..7216e5529736ae77a22654823e6f237b3607b764
--- /dev/null
+++ b/resources/qml/ui/media/MediaControls.qml
@@ -0,0 +1,244 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import "../"
+import "../../"
+import QtMultimedia 5.15
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import im.nheko 1.0
+
+Rectangle {
+    id: control
+
+    property alias desiredVolume: volumeSlider.desiredVolume
+    property bool muted: false
+    property bool playingVideo: false
+    property var mediaState
+    property bool mediaLoaded: false
+    property var duration
+    property var positionValue: 0
+    property var position
+    property bool shouldShowControls: !playingVideo || playerMouseArea.shouldShowControls || volumeSlider.state == "shown"
+
+    signal playPauseActivated()
+    signal loadActivated()
+
+    function showControls() {
+        controlHideTimer.restart();
+    }
+
+    function durationToString(duration) {
+        function maybeZeroPrepend(time) {
+            return (time < 10) ? "0" + time.toString() : time.toString();
+        }
+
+        var totalSeconds = Math.floor(duration / 1000);
+        var seconds = totalSeconds % 60;
+        var minutes = (Math.floor(totalSeconds / 60)) % 60;
+        var hours = (Math.floor(totalSeconds / (60 * 24))) % 24;
+        // Always show minutes and don't prepend zero into the leftmost element
+        var ss = maybeZeroPrepend(seconds);
+        var mm = (hours > 0) ? maybeZeroPrepend(minutes) : minutes.toString();
+        var hh = hours.toString();
+        if (hours < 1)
+            return mm + ":" + ss;
+
+        return hh + ":" + mm + ":" + ss;
+    }
+
+    color: {
+        var wc = Nheko.colors.alternateBase;
+        return Qt.rgba(wc.r, wc.g, wc.b, 0.5);
+    }
+    opacity: control.shouldShowControls ? 1 : 0
+    height: controlLayout.implicitHeight
+
+    HoverHandler {
+        id: playerMouseArea
+
+        property bool shouldShowControls: hovered || controlHideTimer.running || control.mediaState != MediaPlayer.PlayingState
+
+        onHoveredChanged: showControls()
+    }
+
+    ColumnLayout {
+        id: controlLayout
+
+        enabled: control.shouldShowControls
+        spacing: 0
+        anchors.bottom: control.bottom
+        anchors.left: control.left
+        anchors.right: control.right
+
+        NhekoSlider {
+            Layout.fillWidth: true
+            Layout.leftMargin: Nheko.paddingSmall
+            Layout.rightMargin: Nheko.paddingSmall
+            enabled: control.mediaLoaded
+            value: control.positionValue
+            onMoved: control.position = value
+            from: 0
+            to: control.duration
+            alwaysShowSlider: false
+        }
+
+        RowLayout {
+            Layout.margins: Nheko.paddingSmall
+            spacing: Nheko.paddingSmall
+            Layout.fillWidth: true
+
+            // Cache/Play/pause button
+            ImageButton {
+                id: playbackStateImage
+
+                Layout.alignment: Qt.AlignLeft
+                buttonTextColor: Nheko.colors.text
+                Layout.preferredHeight: 24
+                Layout.preferredWidth: 24
+                image: {
+                    if (control.mediaLoaded) {
+                        if (control.mediaState == MediaPlayer.PlayingState)
+                            return ":/icons/icons/ui/pause-symbol.png";
+                        else
+                            return ":/icons/icons/ui/play-sign.png";
+                    } else {
+                        return ":/icons/icons/ui/arrow-pointing-down.png";
+                    }
+                }
+                onClicked: control.mediaLoaded ? control.playPauseActivated() : control.loadActivated()
+            }
+
+            ImageButton {
+                id: volumeButton
+
+                Layout.alignment: Qt.AlignLeft
+                buttonTextColor: Nheko.colors.text
+                Layout.preferredHeight: 24
+                Layout.preferredWidth: 24
+                image: {
+                    if (control.muted || control.desiredVolume <= 0)
+                        return ":/icons/icons/ui/volume-off-indicator.png";
+                    else
+                        return ":/icons/icons/ui/volume-up.png";
+                }
+                onClicked: control.muted = !control.muted
+            }
+
+            NhekoSlider {
+                id: volumeSlider
+
+                property real desiredVolume: QtMultimedia.convertVolume(volumeSlider.value, QtMultimedia.LogarithmicVolumeScale, QtMultimedia.LinearVolumeScale)
+
+                state: ""
+                Layout.alignment: Qt.AlignLeft
+                Layout.preferredWidth: 0
+                opacity: 0
+                orientation: Qt.Horizontal
+                value: 1
+                onDesiredVolumeChanged: {
+                    control.muted = !(desiredVolume > 0);
+                }
+                transitions: [
+                    Transition {
+                        from: ""
+                        to: "shown"
+
+                        SequentialAnimation {
+                            PauseAnimation {
+                                duration: 50
+                            }
+
+                            NumberAnimation {
+                                duration: 100
+                                properties: "opacity"
+                                easing.type: Easing.InQuad
+                            }
+
+                        }
+
+                        NumberAnimation {
+                            properties: "Layout.preferredWidth"
+                            duration: 150
+                        }
+
+                    },
+                    Transition {
+                        from: "shown"
+                        to: ""
+
+                        SequentialAnimation {
+                            PauseAnimation {
+                                duration: 100
+                            }
+
+                            ParallelAnimation {
+                                NumberAnimation {
+                                    duration: 100
+                                    properties: "opacity"
+                                    easing.type: Easing.InQuad
+                                }
+
+                                NumberAnimation {
+                                    properties: "Layout.preferredWidth"
+                                    duration: 150
+                                }
+
+                            }
+
+                        }
+
+                    }
+                ]
+
+                states: State {
+                    name: "shown"
+                    when: Settings.mobileMode || volumeButton.hovered || volumeSlider.hovered || volumeSlider.pressed
+
+                    PropertyChanges {
+                        target: volumeSlider
+                        Layout.preferredWidth: 100
+                    }
+
+                    PropertyChanges {
+                        target: volumeSlider
+                        opacity: 1
+                    }
+
+                }
+
+            }
+
+            Label {
+                Layout.alignment: Qt.AlignRight
+                text: (!control.mediaLoaded) ? "-- / --" : (durationToString(control.positionValue) + " / " + durationToString(control.duration))
+                color: Nheko.colors.text
+            }
+
+            Item {
+                Layout.fillWidth: true
+            }
+
+        }
+
+    }
+
+    // For hiding controls on stationary cursor
+    Timer {
+        id: controlHideTimer
+
+        interval: 1500 //ms
+        repeat: false
+    }
+
+    // Fade controls in/out
+    Behavior on opacity {
+        OpacityAnimator {
+            duration: 100
+        }
+
+    }
+
+}
diff --git a/resources/qml/ui/media/qmldir b/resources/qml/ui/media/qmldir
new file mode 100644
index 0000000000000000000000000000000000000000..143b603da9b0e270813128751cb96021ac3505b1
--- /dev/null
+++ b/resources/qml/ui/media/qmldir
@@ -0,0 +1,3 @@
+module im.nheko.UI.Media
+VolumeSlider 1.0 VolumeSlider.qml
+MediaControls 1.0 MediaControls.qml
\ No newline at end of file
diff --git a/resources/qml/ui/qmldir b/resources/qml/ui/qmldir
index 831a723dca42e74db49ea007efd23cdf01acb6a5..a2ce7514b70056a7ff847c9acf49dc1a4e5f28e2 100644
--- a/resources/qml/ui/qmldir
+++ b/resources/qml/ui/qmldir
@@ -1,3 +1,4 @@
 module im.nheko.UI
+NhekoSlider 1.0 NhekoSlider.qml
 Ripple 1.0 Ripple.qml
 Spinner 1.0 Spinner.qml
\ No newline at end of file
diff --git a/resources/res.qrc b/resources/res.qrc
index 66b77205c4d63f61ae11a4b1469399f24665fc17..a60f4ab03760988b7b598475cbc41f837794adb1 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -3,6 +3,7 @@
         <file>icons/ui/at-solid.svg</file>
         <file>icons/ui/volume-off-indicator.png</file>
         <file>icons/ui/volume-off-indicator@2x.png</file>
+        <file>icons/ui/volume-up.png</file>
         <file>icons/ui/black-bubble-speech.png</file>
         <file>icons/ui/black-bubble-speech@2x.png</file>
         <file>icons/ui/do-not-disturb-rounded-sign.png</file>
@@ -179,9 +180,11 @@
         <file>qml/dialogs/UserProfile.qml</file>
         <file>qml/emoji/EmojiPicker.qml</file>
         <file>qml/emoji/StickerPicker.qml</file>
+        <file>qml/ui/NhekoSlider.qml</file>
         <file>qml/ui/Ripple.qml</file>
         <file>qml/ui/Spinner.qml</file>
         <file>qml/ui/animations/BlinkAnimation.qml</file>
+        <file>qml/ui/media/MediaControls.qml</file>
         <file>qml/voip/ActiveCallBar.qml</file>
         <file>qml/voip/CallDevices.qml</file>
         <file>qml/voip/CallInvite.qml</file>
diff --git a/src/ui/MxcMediaProxy.cpp b/src/ui/MxcMediaProxy.cpp
index db8c0f1f36e24de21fc1487191e25be6a7a90032..df0298dab895cc633f502097d40b934c209e27c3 100644
--- a/src/ui/MxcMediaProxy.cpp
+++ b/src/ui/MxcMediaProxy.cpp
@@ -13,6 +13,11 @@
 #include <QStandardPaths>
 #include <QUrl>
 
+#if defined(Q_OS_MACOS)
+// TODO (red_sky): Remove for Qt6.  See other ifdef below
+#include <QTemporaryFile>
+#endif
+
 #include "EventAccessors.h"
 #include "Logging.h"
 #include "MatrixClient.h"
@@ -75,7 +80,7 @@ MxcMediaProxy::startDownload()
 
     QPointer<MxcMediaProxy> self = this;
 
-    auto processBuffer = [this, encryptionInfo, filename, self](QIODevice &device) {
+    auto processBuffer = [this, encryptionInfo, filename, self, suffix](QIODevice &device) {
         if (!self)
             return;
 
@@ -90,10 +95,34 @@ MxcMediaProxy::startDownload()
         buffer.open(QIODevice::ReadOnly);
         buffer.reset();
 
-        QTimer::singleShot(0, this, [this, filename] {
+        QTimer::singleShot(0, this, [this, filename, suffix, encryptionInfo] {
+#if defined(Q_OS_MACOS)
+            if (encryptionInfo) {
+                // macOS has issues reading from a buffer in setMedia for whatever reason.
+                // Instead, write the buffer to a temporary file and read from that.
+                // This should be fixed in Qt6, so update this when we do that!
+                // TODO: REMOVE IN QT6
+                QTemporaryFile tempFile;
+                tempFile.setFileTemplate(tempFile.fileTemplate() + QLatin1Char('.') + suffix);
+                tempFile.open();
+                tempFile.write(buffer.data());
+                tempFile.close();
+                nhlog::ui()->debug("Playing media from temp buffer file: {}.  Remove in QT6!",
+                                   filename.filePath().toStdString());
+                this->setMedia(QUrl::fromLocalFile(tempFile.fileName()));
+            } else {
+                nhlog::ui()->info(
+                  "Playing buffer with size: {}, {}", buffer.bytesAvailable(), buffer.isOpen());
+                this->setMedia(QUrl::fromLocalFile(filename.filePath()));
+            }
+#else
+            Q_UNUSED(suffix)
+            Q_UNUSED(encryptionInfo)
+
             nhlog::ui()->info(
               "Playing buffer with size: {}, {}", buffer.bytesAvailable(), buffer.isOpen());
             this->setMedia(QMediaContent(filename.fileName()), &buffer);
+#endif
             emit loadedChanged();
         });
     };