diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/opengraph.ex
index 190377767ddf4c534cecbcc4be0c2a9759ddfb20..cafb8134b06b8405ba0c2d98800136431b5f4cf1 100644
--- a/lib/pleroma/web/metadata/opengraph.ex
+++ b/lib/pleroma/web/metadata/opengraph.ex
@@ -3,12 +3,10 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
-  alias Pleroma.HTML
-  alias Pleroma.Formatter
   alias Pleroma.User
   alias Pleroma.Web.Metadata
-  alias Pleroma.Web.MediaProxy
   alias Pleroma.Web.Metadata.Providers.Provider
+  alias Pleroma.Web.Metadata.Utils
 
   @behaviour Provider
 
@@ -19,7 +17,7 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
         user: user
       }) do
     attachments = build_attachments(object)
-    scrubbed_content = scrub_html_and_truncate(object)
+    scrubbed_content = Utils.scrub_html_and_truncate(object)
     # Zero width space
     content =
       if scrubbed_content != "" and scrubbed_content != "\u200B" do
@@ -44,13 +42,14 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
       {:meta,
        [
          property: "og:description",
-         content: "#{user_name_string(user)}" <> content
+         content: "#{Utils.user_name_string(user)}" <> content
        ], []},
       {:meta, [property: "og:type", content: "website"], []}
     ] ++
       if attachments == [] or Metadata.activity_nsfw?(object) do
         [
-          {:meta, [property: "og:image", content: attachment_url(User.avatar_url(user))], []},
+          {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))],
+           []},
           {:meta, [property: "og:image:width", content: 150], []},
           {:meta, [property: "og:image:height", content: 150], []}
         ]
@@ -61,17 +60,17 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
 
   @impl Provider
   def build_tags(%{user: user}) do
-    with truncated_bio = scrub_html_and_truncate(user.bio || "") do
+    with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do
       [
         {:meta,
          [
            property: "og:title",
-           content: user_name_string(user)
+           content: Utils.user_name_string(user)
          ], []},
         {:meta, [property: "og:url", content: User.profile_url(user)], []},
         {:meta, [property: "og:description", content: truncated_bio], []},
         {:meta, [property: "og:type", content: "website"], []},
-        {:meta, [property: "og:image", content: attachment_url(User.avatar_url(user))], []},
+        {:meta, [property: "og:image", content: Utils.attachment_url(User.avatar_url(user))], []},
         {:meta, [property: "og:image:width", content: 150], []},
         {:meta, [property: "og:image:height", content: 150], []}
       ]
@@ -93,14 +92,15 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
           case media_type do
             "audio" ->
               [
-                {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []}
+                {:meta,
+                 [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []}
                 | acc
               ]
 
             "image" ->
               [
-                {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])],
-                 []},
+                {:meta,
+                 [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []},
                 {:meta, [property: "og:image:width", content: 150], []},
                 {:meta, [property: "og:image:height", content: 150], []}
                 | acc
@@ -108,7 +108,8 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
 
             "video" ->
               [
-                {:meta, [property: "og:" <> media_type, content: attachment_url(url["href"])], []}
+                {:meta,
+                 [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []}
                 | acc
               ]
 
@@ -120,37 +121,4 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do
       acc ++ rendered_tags
     end)
   end
-
-  defp scrub_html_and_truncate(%{data: %{"content" => content}} = object) do
-    content
-    # html content comes from DB already encoded, decode first and scrub after
-    |> HtmlEntities.decode()
-    |> String.replace(~r/<br\s?\/?>/, " ")
-    |> HTML.get_cached_stripped_html_for_object(object, __MODULE__)
-    |> Formatter.demojify()
-    |> Formatter.truncate()
-  end
-
-  defp scrub_html_and_truncate(content) when is_binary(content) do
-    content
-    # html content comes from DB already encoded, decode first and scrub after
-    |> HtmlEntities.decode()
-    |> String.replace(~r/<br\s?\/?>/, " ")
-    |> HTML.strip_tags()
-    |> Formatter.demojify()
-    |> Formatter.truncate()
-  end
-
-  defp attachment_url(url) do
-    MediaProxy.url(url)
-  end
-
-  defp user_name_string(user) do
-    "#{user.name} " <>
-      if user.local do
-        "(@#{user.nickname}@#{Pleroma.Web.Endpoint.host()})"
-      else
-        "(@#{user.nickname})"
-      end
-  end
 end
diff --git a/lib/pleroma/web/metadata/player_view.ex b/lib/pleroma/web/metadata/player_view.ex
new file mode 100644
index 0000000000000000000000000000000000000000..e9a8cfc8dd24462a66998c5c48fa2fd9895709d7
--- /dev/null
+++ b/lib/pleroma/web/metadata/player_view.ex
@@ -0,0 +1,21 @@
+defmodule Pleroma.Web.Metadata.PlayerView do
+  use Pleroma.Web, :view
+  import Phoenix.HTML.Tag, only: [content_tag: 3, tag: 2]
+
+  def render("player.html", %{"mediaType" => type, "href" => href}) do
+    {tag_type, tag_attrs} =
+      case type do
+        "audio" <> _ -> {:audio, []}
+        "video" <> _ -> {:video, [loop: true]}
+      end
+
+    content_tag(
+      tag_type,
+      [
+        tag(:source, src: href, type: type),
+        "Your browser does not support #{type} playback."
+      ],
+      [controls: true] ++ tag_attrs
+    )
+  end
+end
diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex
index 32b979357b30bd93d5097954bae903efdb3e8a3d..d672b397f8cf9006935c6f41b73af95f8d0a7ba5 100644
--- a/lib/pleroma/web/metadata/twitter_card.ex
+++ b/lib/pleroma/web/metadata/twitter_card.ex
@@ -3,44 +3,122 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.Metadata.Providers.TwitterCard do
-  alias Pleroma.Web.Metadata.Providers.Provider
+  alias Pleroma.User
   alias Pleroma.Web.Metadata
+  alias Pleroma.Web.Metadata.Providers.Provider
+  alias Pleroma.Web.Metadata.Utils
 
   @behaviour Provider
 
   @impl Provider
-  def build_tags(%{object: object}) do
-    if Metadata.activity_nsfw?(object) or object.data["attachment"] == [] do
-      build_tags(nil)
-    else
-      case find_first_acceptable_media_type(object) do
-        "image" ->
-          [{:meta, [property: "twitter:card", content: "summary_large_image"], []}]
-
-        "audio" ->
-          [{:meta, [property: "twitter:card", content: "player"], []}]
-
-        "video" ->
-          [{:meta, [property: "twitter:card", content: "player"], []}]
-
-        _ ->
-          build_tags(nil)
+  def build_tags(%{
+        activity_id: id,
+        object: object,
+        user: user
+      }) do
+    attachments = build_attachments(id, object)
+    scrubbed_content = Utils.scrub_html_and_truncate(object)
+    # Zero width space
+    content =
+      if scrubbed_content != "" and scrubbed_content != "\u200B" do
+        "“" <> scrubbed_content <> "”"
+      else
+        ""
+      end
+
+    [
+      {:meta,
+       [
+         property: "twitter:title",
+         content: Utils.user_name_string(user)
+       ], []},
+      {:meta,
+       [
+         property: "twitter:description",
+         content: content
+       ], []}
+    ] ++
+      if attachments == [] or Metadata.activity_nsfw?(object) do
+        [
+          {:meta,
+           [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []},
+          {:meta, [property: "twitter:card", content: "summary_large_image"], []}
+        ]
+      else
+        attachments
       end
-    end
   end
 
   @impl Provider
-  def build_tags(_) do
-    [{:meta, [property: "twitter:card", content: "summary"], []}]
+  def build_tags(%{user: user}) do
+    with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do
+      [
+        {:meta,
+         [
+           property: "twitter:title",
+           content: Utils.user_name_string(user)
+         ], []},
+        {:meta, [property: "twitter:description", content: truncated_bio], []},
+        {:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))],
+         []},
+        {:meta, [property: "twitter:card", content: "summary"], []}
+      ]
+    end
   end
 
-  def find_first_acceptable_media_type(%{data: %{"attachment" => attachment}}) do
-    Enum.find_value(attachment, fn attachment ->
-      Enum.find_value(attachment["url"], fn url ->
-        Enum.find(["image", "audio", "video"], fn media_type ->
-          String.starts_with?(url["mediaType"], media_type)
+  defp build_attachments(id, z = %{data: %{"attachment" => attachments}}) do
+    IO.puts(inspect(z))
+
+    Enum.reduce(attachments, [], fn attachment, acc ->
+      rendered_tags =
+        Enum.reduce(attachment["url"], [], fn url, acc ->
+          media_type =
+            Enum.find(["image", "audio", "video"], fn media_type ->
+              String.starts_with?(url["mediaType"], media_type)
+            end)
+
+          # TODO: Add additional properties to objects when we have the data available.
+          case media_type do
+            "audio" ->
+              [
+                {:meta, [property: "twitter:card", content: "player"], []},
+                {:meta, [property: "twitter:player:width", content: "480"], []},
+                {:meta, [property: "twitter:player:height", content: "80"], []},
+                {:meta, [property: "twitter:player", content: player_url(id)], []}
+                | acc
+              ]
+
+            "image" ->
+              [
+                {:meta, [property: "twitter:card", content: "summary_large_image"], []},
+                {:meta,
+                 [
+                   property: "twitter:player",
+                   content: Utils.attachment_url(url["href"])
+                 ], []}
+                | acc
+              ]
+
+            # TODO: Need the true width and height values here or Twitter renders an iFrame with a bad aspect ratio
+            "video" ->
+              [
+                {:meta, [property: "twitter:card", content: "player"], []},
+                {:meta, [property: "twitter:player", content: player_url(id)], []},
+                {:meta, [property: "twitter:player:width", content: "480"], []},
+                {:meta, [property: "twitter:player:height", content: "480"], []}
+                | acc
+              ]
+
+            _ ->
+              acc
+          end
         end)
-      end)
+
+      acc ++ rendered_tags
     end)
   end
+
+  defp player_url(id) do
+    Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice_player, id)
+  end
 end
diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex
new file mode 100644
index 0000000000000000000000000000000000000000..a166800d4d28d4c54f2f46c57cbd53d7453384a9
--- /dev/null
+++ b/lib/pleroma/web/metadata/utils.ex
@@ -0,0 +1,42 @@
+# Pleroma: A lightweight social networking server
+# Copyright \xc2\xa9 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.Metadata.Utils do
+  alias Pleroma.HTML
+  alias Pleroma.Formatter
+  alias Pleroma.Web.MediaProxy
+
+  def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do
+    content
+    # html content comes from DB already encoded, decode first and scrub after
+    |> HtmlEntities.decode()
+    |> String.replace(~r/<br\s?\/?>/, " ")
+    |> HTML.get_cached_stripped_html_for_object(object, __MODULE__)
+    |> Formatter.demojify()
+    |> Formatter.truncate()
+  end
+
+  def scrub_html_and_truncate(content) when is_binary(content) do
+    content
+    # html content comes from DB already encoded, decode first and scrub after
+    |> HtmlEntities.decode()
+    |> String.replace(~r/<br\s?\/?>/, " ")
+    |> HTML.strip_tags()
+    |> Formatter.demojify()
+    |> Formatter.truncate()
+  end
+
+  def attachment_url(url) do
+    MediaProxy.url(url)
+  end
+
+  def user_name_string(user) do
+    "#{user.name} " <>
+      if user.local do
+        "(@#{user.nickname}@#{Pleroma.Web.Endpoint.host()})"
+      else
+        "(@#{user.nickname})"
+      end
+  end
+end
diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex
index ee2e3d6ec38ea50c6e0e32e71b3a03a000673fea..df723f63889de94c11f66dfa137d3b29de2f86c7 100644
--- a/lib/pleroma/web/ostatus/ostatus_controller.ex
+++ b/lib/pleroma/web/ostatus/ostatus_controller.ex
@@ -156,6 +156,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
             %Object{} = object = Object.normalize(activity.data["object"])
 
             Fallback.RedirectController.redirector_with_meta(conn, %{
+              activity_id: activity.id,
               object: object,
               url:
                 Pleroma.Web.Router.Helpers.o_status_url(
@@ -187,6 +188,30 @@ defmodule Pleroma.Web.OStatus.OStatusController do
     end
   end
 
+  # Returns an HTML embedded <audio> or <video> player suitable for embed iframes.
+  def notice_player(conn, %{"id" => id}) do
+    with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
+         true <- ActivityPub.is_public?(activity),
+         %Object{} = object <- Object.normalize(activity.data["object"]),
+         %{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object,
+         true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do
+      conn
+      |> put_layout(:metadata_player)
+      |> put_resp_header("x-frame-options", "ALLOW")
+      |> put_resp_header(
+        "content-security-policy",
+        "default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;"
+      )
+      |> put_view(Pleroma.Web.Metadata.PlayerView)
+      |> render("player.html", url)
+    else
+      _error ->
+        conn
+        |> put_status(404)
+        |> Fallback.RedirectController.redirector(nil, 404)
+    end
+  end
+
   defp represent_activity(
          conn,
          "activity+json",
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index a372610d4dca1375f05d6b6fc3824eea2d4a64e9..5aebcb3535a1c065dc3b5ce8634b40c2c27e8465 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -505,6 +505,7 @@ defmodule Pleroma.Web.Router do
     get("/objects/:uuid", OStatus.OStatusController, :object)
     get("/activities/:uuid", OStatus.OStatusController, :activity)
     get("/notice/:id", OStatus.OStatusController, :notice)
+    get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
     get("/users/:nickname/feed", OStatus.OStatusController, :feed)
     get("/users/:nickname", OStatus.OStatusController, :feed_redirect)
 
diff --git a/lib/pleroma/web/templates/layout/metadata_player.html.eex b/lib/pleroma/web/templates/layout/metadata_player.html.eex
new file mode 100644
index 0000000000000000000000000000000000000000..460f280944a83d7cbbee59b4bd5705dd7f52434b
--- /dev/null
+++ b/lib/pleroma/web/templates/layout/metadata_player.html.eex
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<body>
+
+<style type="text/css">
+video, audio {
+   width:100%;
+   max-width:600px;
+   height: auto;
+}
+</style>
+
+<%= render @view_module, @view_template, assigns %>
+
+</body>
+</html>