Index: Gruntfile.js
===================================================================
--- Gruntfile.js
+++ Gruntfile.js
@@ -5,12 +5,14 @@
 		BUILD_DIR = 'build/',
 
 		BP_CSS = [
-			'**/*.css'
+			'**/*.css',
+			'**/css-*.php'
 		],
 
 		// CSS exclusions, for excluding files from certain tasks, e.g. rtlcss
 		BP_EXCLUDED_CSS = [
-			'!**/*-rtl.css'
+			'!**/*-rtl.css',
+			'!**/*-rtl.php'
 		],
 
 		BP_JS = [
@@ -105,8 +107,14 @@
 				cwd: SOURCE_DIR,
 				dest: SOURCE_DIR,
 				extDot: 'last',
-				ext: '-rtl.css',
-				src: BP_CSS.concat( BP_EXCLUDED_CSS, BP_EXCLUDED_MISC )
+				src: BP_CSS.concat( BP_EXCLUDED_CSS, BP_EXCLUDED_MISC ),
+				rename: function ( dest, src ) {
+					if ( src.endsWith( '.php' ) ) {
+						return dest + src.replace( '.php', '-rtl.php' );
+					} else {
+						return dest + src.replace( '.css', '-rtl.css' );
+					}
+				}
 			}
 		},
 		checktextdomain: {
Index: src/bp-activity/bp-activity-classes.php
===================================================================
--- src/bp-activity/bp-activity-classes.php
+++ src/bp-activity/bp-activity-classes.php
@@ -13,3 +13,8 @@
 require dirname( __FILE__ ) . '/classes/class-bp-activity-activity.php';
 require dirname( __FILE__ ) . '/classes/class-bp-activity-feed.php';
 require dirname( __FILE__ ) . '/classes/class-bp-activity-query.php';
+
+// Embeds - only applicable for WP 4.5+
+if ( bp_get_major_wp_version() >= 4.5 && bp_is_active( 'activity', 'embeds' ) ) {
+	require dirname( __FILE__ ) . '/classes/class-bp-activity-oembed-component.php';
+}
\ No newline at end of file
Index: src/bp-activity/bp-activity-embeds.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/bp-activity-embeds.php
@@ -0,0 +1,348 @@
+<?php
+/**
+ * Functions related to embedding single activity items externally.
+ *
+ * Relies on WordPress 4.5.
+ *
+ * @since 2.6.0
+ *
+ * @package BuddyPress
+ * @subpackage ActivityEmbeds
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Loads our activity oEmbed component.
+ *
+ * @since 2.6.0
+ */
+function bp_activity_setup_oembed() {
+	if ( bp_get_major_wp_version() >= 4.5 && bp_is_active( 'activity', 'embeds' ) ) {
+		buddypress()->activity->oembed = new BP_Activity_oEmbed_Component;
+	}
+
+	add_filter( 'bp_activity_get_embed_excerpt', 'wptexturize' );
+	add_filter( 'bp_activity_get_embed_excerpt', 'convert_chars' );
+	add_filter( 'bp_activity_get_embed_excerpt', 'make_clickable', 9 );
+	add_filter( 'bp_activity_get_embed_excerpt', 'bp_activity_embed_excerpt_onclick_location_filter' );
+	add_filter( 'bp_activity_get_embed_excerpt', 'bp_activity_at_name_filter' );
+	add_filter( 'bp_activity_get_embed_excerpt', 'convert_smilies', 20 );
+	add_filter( 'bp_activity_get_embed_excerpt', 'wpautop', 30 );
+}
+add_action( 'bp_loaded', 'bp_activity_setup_oembed' );
+
+/**
+ * Catch links in embed excerpt so top.location.href can be added.
+ *
+ * Due to <iframe sandbox="allow-top-navigation">, links in embeds can only be
+ * clicked if invoked with top.location.href via JS.
+ *
+ * @since 2.6.0
+ *
+ * @param  string $text Embed excerpt
+ * @return string
+ */
+function bp_activity_embed_excerpt_onclick_location_filter( $text ) {
+	return preg_replace_callback( '/<a href=\"([^\"]*)\"/iU', 'bp_activity_embed_excerpt_onclick_location_filter_callback', $text );
+}
+	/**
+	 * Add onclick="top.location.href" to a link.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  array $matches Items matched by bp_activity_embed_excerpt_onclick_location_filter().
+	 * @return string
+	 */
+	function bp_activity_embed_excerpt_onclick_location_filter_callback( $matches ) {
+		return sprintf( '<a href="%1$s" onclick="top.location.href=\'%1$s\'"', $matches[1] );
+	}
+
+/**
+ * Add inline styles for BP activity embeds.
+ *
+ * This is subject to change or be removed entirely for a different system.
+ * Potentially for BP_Legacy::locate_asset_in_stack().
+ *
+ * @since  2.6.0
+ * @access private
+ */
+function _bp_activity_embed_add_inline_styles() {
+	if ( false === bp_is_single_activity() ) {
+		return;
+	}
+
+	ob_start();
+	if ( is_rtl() ) {
+		bp_get_asset_template_part( 'embeds/css-activity', 'rtl' );
+	} else {
+		bp_get_asset_template_part( 'embeds/css-activity' );
+	}
+	$css = ob_get_clean();
+
+	// Rudimentary CSS protection.
+	$css = wp_kses( $css, array( "\'", '\"' ) );
+
+	printf( '<style type="text/css">%s</style>', $css );
+}
+add_action( 'embed_head', '_bp_activity_embed_add_inline_styles', 20 );
+
+/**
+ * Query for the activity item on the activity embed template.
+ *
+ * Basically a wrapper for {@link bp_has_activities()}, but allows us to
+ * use the activity loop without requerying for it again.
+ *
+ * @since 2.6.0
+ *
+ * @param  int $activity_id The activity ID.
+ * @return bool
+ */
+function bp_activity_embed_has_activity( $activity_id = 0 ) {
+	global $activities_template;
+
+	if ( empty( $activity_id ) ) {
+		return false;
+	}
+
+	if ( ! empty( $activities_template->activities ) ) {
+		$activity = (array) $activities_template->activities;
+		$activity = reset( $activity );
+
+		// No need to requery if we already got the embed activity
+		if ( (int) $activity_id === (int) $activity->id ) {
+			return $activities_template->has_activities();
+		}
+	}
+
+	return bp_has_activities( array(
+		'display_comments' => 'threaded',
+		'show_hidden'      => true,
+		'include'          => (int) $activity_id,
+	) );
+}
+
+/**
+ * Outputs excerpt for an activity embed item.
+ *
+ * @since 2.6.0
+ */
+function bp_activity_embed_excerpt( $content = '' ) {
+	echo bp_activity_get_embed_excerpt( $content = '' );
+}
+
+	/**
+	 * Generates excerpt for an activity embed item.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  string $content The content to generate an excerpt for.
+	 * @return string
+	 */
+	function bp_activity_get_embed_excerpt( $content = '' ) {
+		if ( empty( $content ) && ! empty( $GLOBALS['activities_template']->in_the_loop ) ) {
+			$content = $GLOBALS['activities_template']->activity->content;
+		}
+
+		/**
+		 * bp_activity_truncate_entry() includes the 'Read More' link, which is why
+		 * we're using this instead of bp_create_excerpt().
+		 */
+		$content = html_entity_decode( $content );
+		$content = bp_activity_truncate_entry( $content, array(
+			'html' => false,
+			'filter_shortcodes' => true,
+			'strip_tags'        => true,
+			'force_truncate'    => true
+		) );
+
+		/**
+		 * Filter the activity embed excerpt.
+		 *
+		 * @since 2.6.0
+		 *
+		 * @var string $content Embed Excerpt.
+		 * @var string $unmodified_content Unmodified activity content.
+		 */
+		return apply_filters( 'bp_activity_get_embed_excerpt', $content, $GLOBALS['activities_template']->activity->content );
+	}
+
+/**
+ * Outputs the first embedded item in the activity oEmbed template.
+ *
+ * @since 2.6.0
+ */
+function bp_activity_embed_media() {
+	// Bail if oEmbed request explicitly hides media.
+	if ( isset( $_REQUEST['hide_media'] ) && true == wp_validate_boolean( $_REQUEST['hide_media'] ) ) {
+		/**
+		 * Do something after media is rendered for an activity oEmbed item.
+		 *
+		 * @since 2.6.0
+		 */
+		do_action( 'bp_activity_embed_after_media' );
+
+		return;
+	}
+
+	/**
+	 * Should we display media in the oEmbed template?
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param bool $retval Defaults to true.
+	 */
+	$allow_media = apply_filters( 'bp_activity_embed_display_media', true );
+
+	// Find oEmbeds from only WP registered providers.
+	bp_remove_all_filters( 'oembed_providers' );
+	$media = bp_core_extract_media_from_content( $GLOBALS['activities_template']->activity->content, 'embeds' );
+	bp_restore_all_filters( 'oembed_providers' );
+
+	// oEmbeds have precedence over inline video / audio.
+	if ( isset( $media['embeds'] ) && true === $allow_media ) {
+		// Autoembed first URL.
+		$oembed_defaults = wp_embed_defaults();
+		$oembed_args = array(
+			'width'    => $oembed_defaults['width'],
+			'height'   => $oembed_defaults['height'],
+			'discover' => true
+		);
+		$url      = $media['embeds'][0]['url'];
+		$cachekey = '_oembed_response_' . md5( $url . serialize( $oembed_args ) );
+
+		// Try to fetch oEmbed response from meta.
+		$oembed = bp_activity_get_meta( bp_get_activity_id(), $cachekey );
+
+		// No cache, so fetch full oEmbed response now!
+		if ( '' === $oembed ) {
+			$o = _wp_oembed_get_object();
+			$oembed = $o->fetch( $o->get_provider( $url, $oembed_args ), $url, $oembed_args );
+
+			// Cache oEmbed response.
+			bp_activity_update_meta( bp_get_activity_id(), $cachekey, $oembed );
+		}
+
+		$content = '';
+
+		/**
+		 * Filters the default embed display max width.
+		 *
+		 * This is used if the oEmbed response does not return a thumbnail width.
+		 *
+		 * @since 2.6.0
+		 *
+		 * @param int $width.
+		 */
+		$width = (int) apply_filters( 'bp_activity_embed_display_media_width', 550 );
+
+		// Set thumbnail.
+		if ( 'photo' === $oembed->type ) {
+			$thumbnail = $oembed->url;
+		} elseif ( isset( $oembed->thumbnail_url ) ) {
+			$thumbnail = $oembed->thumbnail_url;
+
+		/* Non-oEmbed standard attributes */
+		// Mixcloud
+		} elseif ( isset( $oembed->image ) ) {
+			$thumbnail = $oembed->image;
+		// ReverbNation
+		} elseif ( isset( $oembed->{'thumbnail-url'} ) ) {
+			$thumbnail = $oembed->{'thumbnail-url'};
+		}
+
+		// Display thumb and related oEmbed meta.
+		if ( true === isset ( $thumbnail ) ) {
+			$play_icon = $caption = '';
+
+			// Add play icon for non-photos.
+			if ( 'photo' !== $oembed->type ) {
+				/**
+				 * ion-play icon from Ionicons.
+				 *
+				 * @link    http://ionicons.com/
+				 * @license MIT
+				 */
+				$play_icon = <<<EOD
+<svg id="Layer_1" style="enable-background:new 0 0 512 512;" version="1.1" viewBox="0 0 512 512" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M405.2,232.9L126.8,67.2c-3.4-2-6.9-3.2-10.9-3.2c-10.9,0-19.8,9-19.8,20H96v344h0.1c0,11,8.9,20,19.8,20  c4.1,0,7.5-1.4,11.2-3.4l278.1-165.5c6.6-5.5,10.8-13.8,10.8-23.1C416,246.7,411.8,238.5,405.2,232.9z"/></svg>
+EOD;
+
+				$play_icon = sprintf( '<a rel="nofollow" class="play-btn" href="%1$s">%2$s</a>', esc_url( $url ), $play_icon );
+			}
+
+			// Thumb width
+			$width = isset( $oembed->thumbnail_width ) && 'photo' !== $oembed->type && (int) $oembed->thumbnail_width < 550 ? (int) $oembed->thumbnail_width : $width;
+
+			// Set up thumb.
+			$content = sprintf( '<div class="thumb" style="max-width:%1$spx">%2$s<a href="%3$s" rel="nofollow" onclick="top.location.href=\'%3$s\'"><img src="%4$s" /></a></div>', $width, $play_icon, esc_url( $url ), esc_url( $thumbnail ) );
+
+			// Show title.
+			if ( isset( $oembed->title ) ) {
+				$caption .= sprintf( '<p class="caption-title"><strong>%s</strong></p>', apply_filters( 'single_post_title', $oembed->title ) );
+			}
+
+			// Show description (non-oEmbed standard)
+			if ( isset( $oembed->description ) ) {
+				$caption .= sprintf( '<div class="caption-description">%s</div>', apply_filters( 'bp_get_activity_action', $oembed->description ) );
+			}
+
+			// Show author info.
+			if ( isset( $oembed->provider_name ) && isset( $oembed->author_name ) ) {
+				/* translators: By [oEmbed author] on [oEmbed provider]. eg. By BuddyPress on YouTube. */
+				$anchor_text = sprintf( __( 'By %1$s on %2$s', 'buddypress' ), $oembed->author_name, $oembed->provider_name );
+
+			} elseif ( isset( $oembed->provider_name ) ) {
+				$anchor_text = sprintf( __( 'View on %s', 'buddypress' ), $oembed->provider_name );
+			}
+
+			if ( true === isset( $anchor_text ) )  {
+				$caption .= sprintf( '<a rel="nofollow" href="%1$s" onclick="top.location.href=\'%1$s\'">%2$s</a>', esc_url( $url ), apply_filters( 'the_title', $anchor_text ) );
+			}
+
+			// Set up caption.
+			if ( '' !== $caption ) {
+				$css_class = isset( $oembed->provider_name ) ? sprintf( ' provider-%s', sanitize_html_class( strtolower( $oembed->provider_name ) ) ) : '';
+				$caption = sprintf( '<div class="caption%1$s">%2$s</div>', $css_class, $caption );
+
+				$content .= $caption;
+			}
+		}
+
+		// Print rich content.
+		if ( '' !== $content ) {
+			printf( '<div class="bp-activity-embed-display-media" style="max-width:%spx">%s</div>', $width, $content );
+		}
+
+	// Video / audio.
+	} elseif ( true === $allow_media ) {
+		// Call BP_Embed if it hasn't already loaded.
+		bp_embed_init();
+
+		// Run shortcode and embed routine.
+		$content = buddypress()->embed->run_shortcode( $GLOBALS['activities_template']->activity->content );
+		$content = buddypress()->embed->autoembed( $content );
+
+		// Try to find inline video / audio.
+		$media = bp_core_extract_media_from_content( $content, 96 );
+
+		// Video takes precedence. HTML5-only.
+		if ( isset( $media['videos'] ) && 'shortcodes' === $media['videos'][0]['source'] ) {
+			printf( '<video controls preload="metadata"><source src="%1$s"><p>%2$s</p></video>',
+				esc_url( $media['videos'][0]['url'] ),
+				esc_html__( 'Your browser does not support HTML5 video', 'buddypress' )
+			);
+
+		// No video? Try audio. HTML5-only.
+		} elseif ( isset( $media['audio'] ) && 'shortcodes' === $media['audio'][0]['source'] ) {
+			printf( '<audio controls preload="metadata"><source src="%1$s"><p>%2$s</p></audio>',
+				esc_url( $media['audio'][0]['url'] ),
+				esc_html__( 'Your browser does not support HTML5 audio', 'buddypress' )
+			);
+		}
+
+	}
+
+	/** This hook is documented in /bp-activity/bp-activity-embeds.php */
+	do_action( 'bp_activity_embed_after_media' );
+}
Index: src/bp-activity/bp-activity-filters.php
===================================================================
--- src/bp-activity/bp-activity-filters.php
+++ src/bp-activity/bp-activity-filters.php
@@ -395,19 +395,12 @@
  * This method can only be used inside the Activity loop.
  *
  * @since 1.5.0
- *
- * @uses bp_is_single_activity()
- * @uses apply_filters() To call the 'bp_activity_excerpt_append_text' hook.
- * @uses apply_filters() To call the 'bp_activity_excerpt_length' hook.
- * @uses bp_create_excerpt()
- * @uses bp_get_activity_id()
- * @uses bp_get_activity_thread_permalink()
- * @uses apply_filters() To call the 'bp_activity_truncate_entry' hook.
+ * @since 2.6.0 Added $args parameter.
  *
  * @param string $text The original activity entry text.
  * @return string $excerpt The truncated text.
  */
-function bp_activity_truncate_entry( $text ) {
+function bp_activity_truncate_entry( $text, $args = array() ) {
 	global $activities_template;
 
 	/**
@@ -423,7 +416,7 @@
 	);
 
 	// The full text of the activity update should always show on the single activity screen.
-	if ( ! $maybe_truncate_text || bp_is_single_activity() ) {
+	if ( empty( $args['force_truncate'] ) && ( ! $maybe_truncate_text || bp_is_single_activity() ) ) {
 		return $text;
 	}
 
@@ -445,15 +438,17 @@
 	 */
 	$excerpt_length = apply_filters( 'bp_activity_excerpt_length', 358 );
 
+	$args = wp_parse_args( $args, array( 'ending' => __( '&hellip;', 'buddypress' ) ) );
+
 	// Run the text through the excerpt function. If it's too short, the original text will be returned.
-	$excerpt        = bp_create_excerpt( $text, $excerpt_length, array( 'ending' => __( '&hellip;', 'buddypress' ) ) );
+	$excerpt        = bp_create_excerpt( $text, $excerpt_length, $args );
 
 	/*
 	 * If the text returned by bp_create_excerpt() is different from the original text (ie it's
 	 * been truncated), add the "Read More" link. Note that bp_create_excerpt() is stripping
 	 * shortcodes, so we have strip them from the $text before the comparison.
 	 */
-	if ( $excerpt != strip_shortcodes( $text ) ) {
+	if ( strlen( $excerpt ) > strlen( strip_shortcodes( $text ) ) ) {
 		$id = !empty( $activities_template->activity->current_comment->id ) ? 'acomment-read-more-' . $activities_template->activity->current_comment->id : 'activity-read-more-' . bp_get_activity_id();
 
 		$excerpt = sprintf( '%1$s<span class="activity-read-more" id="%2$s"><a href="%3$s" rel="nofollow">%4$s</a></span>', $excerpt, $id, bp_get_activity_thread_permalink(), $append_text );
Index: src/bp-activity/bp-activity-functions.php
===================================================================
--- src/bp-activity/bp-activity-functions.php
+++ src/bp-activity/bp-activity-functions.php
@@ -3338,8 +3338,12 @@
 	}
 
 	// Generate a text excerpt for this activity item (and remove any oEmbeds URLs).
-	$summary = strip_shortcodes( html_entity_decode( strip_tags( $content ) ) );
-	$summary = bp_create_excerpt( preg_replace( '#^\s*(https?://[^\s"]+)\s*$#im', '', $summary ) );
+	$summary = bp_create_excerpt( html_entity_decode( $content ), 225, array(
+		'html' => false,
+		'filter_shortcodes' => true,
+		'strip_tags'        => true,
+		'remove_links'      => true
+	) );
 
 	if ( $use_media_type === 'embeds' ) {
 		$summary .= PHP_EOL . PHP_EOL . $extracted_media['url'];
@@ -3500,12 +3504,29 @@
  */
 function bp_activity_embed() {
 	add_filter( 'embed_post_id',         'bp_get_activity_id'                  );
+	add_filter( 'oembed_dataparse',      'bp_activity_oembed_dataparse', 10, 2 );
 	add_filter( 'bp_embed_get_cache',    'bp_embed_activity_cache',      10, 3 );
 	add_action( 'bp_embed_update_cache', 'bp_embed_activity_save_cache', 10, 3 );
 }
 add_action( 'activity_loop_start', 'bp_activity_embed' );
 
 /**
+ * Cache full oEmbed response from oEmbed.
+ *
+ * @since 2.6.0
+ *
+ * @param string $retval Current oEmbed result.
+ * @param object $data   Full oEmbed response.
+ * @param string $url    URL used for the oEmbed request.
+ * @return string
+ */
+function bp_activity_oembed_dataparse( $retval, $data ) {
+	buddypress()->activity->oembed_response = $data;
+
+	return $retval;
+}
+
+/**
  * Set up activity oEmbed cache while recursing through activity comments.
  *
  * While crawling through an activity comment tree
@@ -3605,6 +3626,12 @@
  */
 function bp_embed_activity_save_cache( $cache, $cachekey, $id ) {
 	bp_activity_update_meta( $id, $cachekey, $cache );
+
+	// Cache full oEmbed response.
+	if ( true === isset( buddypress()->activity->oembed_response ) ) {
+		$cachekey = str_replace( '_oembed', '_oembed_response', $cachekey );
+		bp_activity_update_meta( $id, $cachekey, buddypress()->activity->oembed_response );
+	}
 }
 
 /**
Index: src/bp-activity/classes/class-bp-activity-component.php
===================================================================
--- src/bp-activity/classes/class-bp-activity-component.php
+++ src/bp-activity/classes/class-bp-activity-component.php
@@ -32,6 +32,7 @@
 			array(
 				'adminbar_myaccount_order' => 10,
 				'search_query_arg' => 'activity_search',
+				'features' => array( 'embeds' )
 			)
 		);
 	}
@@ -72,6 +73,11 @@
 			$includes[] = 'akismet';
 		}
 
+		// Embeds - only applicable for WP 4.5+
+		if ( bp_get_major_wp_version() >= 4.5 && bp_is_active( $this->id, 'embeds' ) ) {
+			$includes[] = 'embeds';
+		}
+
 		if ( is_admin() ) {
 			$includes[] = 'admin';
 		}
Index: src/bp-activity/classes/class-bp-activity-oembed-component.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-activity/classes/class-bp-activity-oembed-component.php
@@ -0,0 +1,326 @@
+<?php
+/**
+ * BuddyPress Activity Classes.
+ *
+ * @package BuddyPress
+ * @subpackage Embeds
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+require_once( buddypress()->plugin_dir . '/bp-core/classes/class-bp-oembed-component.php' );
+
+/**
+ * oEmbed handler to respond and render single activity items.
+ *
+ * @since 2.6.0
+ */
+class BP_Activity_oEmbed_Component extends BP_oEmbed_Component {
+	/**
+	 * Custom oEmbed slug endpoint.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @var string
+	 */
+	public $slug_endpoint = 'activity';
+
+	/**
+	 * Custom hooks.
+	 *
+	 * @since 2.6.0
+	 */
+	protected function custom_hooks() {
+		add_action( 'oembed_dataparse',   array( $this, 'use_custom_iframe_sandbox_attribute' ), 20, 3 );
+		add_action( 'embed_content_meta', array( $this, 'embed_comments_button' ), 5 );
+		add_action( 'get_template_part_assets/embeds/header', array( $this, 'on_activity_header' ), 10, 2 );
+
+		add_filter( 'bp_activity_embed_html', array( $this, 'modify_iframe' ) );
+	}
+
+	/**
+	 * Add custom endpoint arguments.
+	 *
+	 * Currently, includes 'hide_media'.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @return array
+	 */
+	protected function set_route_args() {
+		return array(
+			'hide_media' => array(
+				'default' => false,
+				'sanitize_callback' => 'wp_validate_boolean'
+			)
+		);
+	}
+
+	/**
+	 * Output our custom embed template part.
+	 *
+	 * @since 2.6.0
+	 */
+	protected function content() {
+		bp_get_asset_template_part( 'embeds/activity' );
+	}
+
+	/**
+	 * Check if we're on our single activity page.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @return bool
+	 */
+	protected function is_page() {
+		return bp_is_single_activity();
+	}
+
+	/**
+	 * Validates the URL to determine if the activity item is valid.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  string   $url The URL to check.
+	 * @return int|bool Activity ID on success; boolean false on failure.
+	 */
+	protected function validate_url_to_item_id( $url ) {
+		if ( bp_core_enable_root_profiles() ) {
+			$domain = bp_get_root_domain();
+		} else {
+			$domain = bp_get_members_directory_permalink();
+		}
+
+		// Check the URL to see if this is a single activity URL.
+		if ( 0 !== strpos( $url, $domain ) ) {
+			return false;
+		}
+
+		// Check for activity slug.
+		if ( false === strpos( $url, '/' . bp_get_activity_slug() . '/' ) ) {
+			return false;
+		}
+
+		// Do more checks.
+		$url = trim( untrailingslashit( $url ) );
+
+		// Grab the activity ID.
+		$activity_id = (int) substr(
+			$url,
+			strrpos( $url, '/' ) + 1
+		);
+
+		if ( ! empty( $activity_id ) ) {
+			// Check if activity item still exists.
+			$activity = new BP_Activity_Activity( $activity_id );
+
+			// Okay, we're good to go!
+			if ( ! empty( $activity->component ) && 0 === (int) $activity->is_spam ) {
+				return $activity_id;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * Sets the oEmbed response data for our activity item.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  int $item_id The activity ID.
+	 * @return array
+	 */
+	protected function set_oembed_response_data( $item_id ) {
+		$activity = new BP_Activity_Activity( $item_id );
+
+		return array(
+			'user_id'    => $activity->user_id,
+			'content'    => $activity->content,
+			'title'      => __( 'Activity', 'buddypress' ),
+			'author_url' => bp_core_get_user_domain( $activity->user_id )
+		);
+	}
+
+	/**
+	 * Sets a custom <blockquote> for our oEmbed fallback HTML.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  int $item_id The activity ID.
+	 * @return string
+	 */
+	protected function set_fallback_html( $item_id ) {
+		$activity    = new BP_Activity_Activity( $item_id );
+		$mentionname = bp_activity_do_mentions() ? ' (@' . bp_activity_get_user_mentionname( $activity->user_id ) . ')' : '';
+		$date        = date_i18n( get_option( 'date_format' ), strtotime( $activity->date_recorded ) );
+
+		// Make sure we can use some activity functions that depend on the loop.
+		$GLOBALS['activities_template'] = new stdClass;
+		$GLOBALS['activities_template']->activity = $activity;
+
+		// 'wp-embedded-content' CSS class is necessary due to how the embed JS works.
+		$blockquote = sprintf( '<blockquote class="wp-embedded-content bp-activity-item">%1$s%2$s %3$s</blockquote>',
+			bp_activity_get_embed_excerpt( $activity->content ),
+			'- ' . bp_core_get_user_displayname( $activity->user_id ) . $mentionname,
+			'<a href="' . esc_url( bp_activity_get_permalink( $item_id ) ) . '">' . $date . '</a>'
+		);
+
+		// Clean up.
+		unset( $GLOBALS['activities_template'] );
+
+		/**
+		 * Filters the fallback HTML used when embedding a BP activity item.
+		 *
+		 * @since 2.6.0
+		 *
+		 * @param string               $blockquote Current fallback HTML
+		 * @param BP_Activity_Activity $activity   Activity object
+		 */
+		return apply_filters( 'bp_activity_embed_fallback_html', $blockquote, $activity );
+	}
+
+	/**
+	 * Sets a custom <iframe> title for our oEmbed item.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  int $item_id The activity ID
+	 * @return string
+	 */
+	protected function set_iframe_title( $item_id ) {
+		return __( 'Embedded Activity Item', 'buddypress' );
+	}
+
+	/**
+	 * Use our custom <iframe> sandbox attribute in our oEmbed response.
+	 *
+	 * WordPress sets the <iframe> sandbox attribute to 'allow-scripts' regardless
+	 * of whatever the oEmbed response is in {@link wp_filter_oembed_result()}. We
+	 * need to add back our custom sandbox value so links will work.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @see BP_Activity_Component::modify_iframe() where our custom sandbox value is set.
+	 *
+	 * @param string $result The oEmbed HTML result.
+	 * @param object $data   A data object result from an oEmbed provider.
+	 * @param string $url    The URL of the content to be embedded.
+	 * @return string
+	 */
+	public function use_custom_iframe_sandbox_attribute( $result, $data, $url ) {
+		// Make sure we are on our activity embed URL. If not, bail.
+		if ( false === $this->validate_url_to_item_id( $url ) ) {
+			return $result;
+		}
+
+		// Get unfiltered sandbox attribute from our own oEmbed response.
+		$sandbox_pos = strpos( $data->html, 'sandbox=' ) + 9;
+		$sandbox = substr( $data->html, $sandbox_pos, strpos( $data->html, '"', $sandbox_pos ) - $sandbox_pos );
+
+		// Replace only if our sandbox attribute contains 'allow-top-navigation'.
+		if ( false !== strpos( $sandbox, 'allow-top-navigation' ) ) {
+			$result = str_replace( ' sandbox="allow-scripts"', " sandbox=\"{$sandbox}\"", $result );
+
+			// Also remove 'security' attribute; this is only used for IE < 10.
+			$result = str_replace( 'security="restricted"', "", $result );
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Modify various IFRAME-related items if embeds are allowed.
+	 *
+	 * HTML modified:
+	 *  - Add sandbox="allow-top-navigation" attribute. This allows links to work
+	 *    within the iframe sandbox attribute.
+	 *
+	 * JS modified:
+	 *  - Remove IFRAME height restriction of 1000px. Fixes long embed items being
+	 *    truncated.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  string $retval Current embed HTML.
+	 * @return string
+	 */
+	public function modify_iframe( $retval ) {
+		// Add 'allow-top-navigation' to allow links to be clicked.
+		$retval = str_replace( 'sandbox="', 'sandbox="allow-top-navigation ', $retval );
+
+		// See /wp-includes/js/wp-embed.js.
+		if ( SCRIPT_DEBUG ) {
+			// Removes WP's hardcoded IFRAME height restriction.
+			$retval = str_replace( 'height = 1000;', 'height = height;', $retval );
+
+		// This is for the WP build minified version.
+		} else {
+			$retval = str_replace( 'g=1e3', 'g=g', $retval );
+		}
+
+		return $retval;
+	}
+
+	/**
+	 * Do stuff when our oEmbed activity header template part is loading.
+	 *
+	 * Currently, removes wpautop() from the bp_activity_action() function.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param string $slug Template part slug requested.
+	 * @param string $name Template part name requested.
+	 */
+	public function on_activity_header( $slug, $name ) {
+		if ( false === $this->is_page() || 'activity' !== $name ) {
+			return;
+		}
+
+		remove_filter( 'bp_get_activity_action', 'wpautop' );
+	}
+
+	/**
+	 * Prints the markup for the activity embed comments button.
+	 *
+	 * Basically a copy of {@link print_embed_comments_button()}, but modified for
+	 * the BP activity component.
+	 *
+	 * @since 2.6.0
+	 */
+	public function embed_comments_button() {
+		if ( ! did_action( 'bp_embed_content' ) || ! bp_is_single_activity() ) {
+			return;
+		}
+
+		// Make sure our custom permalink shows up in the 'WordPress Embed' block.
+		add_filter( 'the_permalink', array( $this, 'filter_embed_url' ) );
+
+		// Only show comment bubble if we have some activity comments.
+		$count = bp_activity_get_comment_count();
+		if ( empty( $count ) ) {
+			return;
+		}
+	?>
+
+		<div class="wp-embed-comments">
+			<a href="<?php bp_activity_thread_permalink(); ?>">
+				<span class="dashicons dashicons-admin-comments"></span>
+				<?php
+				printf(
+					_n(
+						'%s <span class="screen-reader-text">Comment</span>',
+						'%s <span class="screen-reader-text">Comments</span>',
+						$count,
+						'buddypress'
+					),
+					number_format_i18n( $count )
+				);
+				?>
+			</a>
+		</div>
+
+	<?php
+	}
+}
Index: src/bp-core/bp-core-actions.php
===================================================================
--- src/bp-core/bp-core-actions.php
+++ src/bp-core/bp-core-actions.php
@@ -40,6 +40,7 @@
 add_action( 'setup_theme',             'bp_setup_theme',            10    );
 add_action( 'after_setup_theme',       'bp_after_setup_theme',      100   ); // After WP themes.
 add_action( 'wp_enqueue_scripts',      'bp_enqueue_scripts',        10    );
+add_action( 'enqueue_embed_scripts',   'bp_enqueue_embed_scripts',  10    );
 add_action( 'admin_bar_menu',          'bp_setup_admin_bar',        20    ); // After WP core.
 add_action( 'template_redirect',       'bp_template_redirect',      10    );
 add_action( 'widgets_init',            'bp_widgets_init',           10    );
Index: src/bp-core/bp-core-dependency.php
===================================================================
--- src/bp-core/bp-core-dependency.php
+++ src/bp-core/bp-core-dependency.php
@@ -477,6 +477,24 @@
 }
 
 /**
+ * Fires the 'bp_enqueue_embed_scripts' action in the <head> for BP oEmbeds.
+ *
+ * @since 2.6.0
+ */
+function bp_enqueue_embed_scripts() {
+	if ( ! is_buddypress() ) {
+		return;
+	}
+
+	/**
+	 * Enqueue CSS and JS files for BuddyPress embeds.
+	 *
+	 * @since 2.6.0
+	 */
+	do_action ( 'bp_enqueue_embed_scripts' );
+}
+
+/**
  * Fire the 'bp_add_rewrite_tag' action, where BP adds its custom rewrite tags.
  *
  * @since 1.8.0
Index: src/bp-core/bp-core-functions.php
===================================================================
--- src/bp-core/bp-core-functions.php
+++ src/bp-core/bp-core-functions.php
@@ -1689,6 +1689,38 @@
 	return apply_filters( 'bp_use_embed_in_private_messages', !defined( 'BP_EMBED_DISABLE_PRIVATE_MESSAGES' ) || !BP_EMBED_DISABLE_PRIVATE_MESSAGES );
 }
 
+/**
+ * Extracts media metadata from a given content.
+ *
+ * @since 2.6.0
+ *
+ * @param  string     $content The content to check.
+ * @param  string|int $type    The type to check. Can also use a bitmask. See the class constants in the
+ *                             BP_Media_Extractor class for more info.
+ * @return array|bool          If media exists, will return array of media metadata. Else, boolean false.
+ */
+function bp_core_extract_media_from_content( $content = '', $type = 'all' ) {
+	if ( is_string( $type ) ) {
+		$class = new ReflectionClass( 'BP_Media_Extractor' );
+		$bitmask = $class->getConstant( strtoupper( $type ) );
+	} else {
+		$bitmask = (int) $type;
+	}
+
+	// Type isn't valid, so bail.
+	if ( empty( $bitmask ) ) {
+		return false;
+	}
+
+	$x = new BP_Media_Extractor;
+	$media = $x->extract( $content, $bitmask );
+
+	unset( $media['has'] );
+	$retval = array_filter( $media );
+
+	return ! empty( $retval ) ? $retval : false;
+}
+
 /** Admin *********************************************************************/
 
 /**
Index: src/bp-core/bp-core-template-loader.php
===================================================================
--- src/bp-core/bp-core-template-loader.php
+++ src/bp-core/bp-core-template-loader.php
@@ -66,6 +66,20 @@
 }
 
 /**
+ * Get an asset template part.
+ *
+ * Basically the same as {@link bp_get_template_part()}, but with 'assets/'
+ * prepended to the slug.
+ *
+ * @since 2.6.0
+ *
+ * @see bp_get_template_part() for full documentation.
+ */
+function bp_get_asset_template_part( $slug, $name = null ) {
+	return bp_get_template_part( "assets/{$slug}", $name );
+}
+
+/**
  * Retrieve the name of the highest priority template file that exists.
  *
  * Searches in the STYLESHEETPATH before TEMPLATEPATH so that themes which
Index: src/bp-core/bp-core-template.php
===================================================================
--- src/bp-core/bp-core-template.php
+++ src/bp-core/bp-core-template.php
@@ -784,6 +784,8 @@
  * This function is borrowed from CakePHP v2.0, under the MIT license. See
  * http://book.cakephp.org/view/1469/Text#truncate-1625
  *
+ * @since 2.6.0 Added 'strip_tags' and 'remove_links' as $options args.
+ *
  * ### Options:
  *
  * - `ending` Will be used as Ending and appended to the trimmed string.
@@ -806,6 +808,10 @@
  *                                     excerpt length. Default: true.
  *     @type bool   $filter_shortcodes If true, shortcodes will be stripped.
  *                                     Default: true.
+ *     @type bool   $strip_tags        If true, HTML tags will be stripped. Default: false.
+ *                                     Only applicable if $html is set to false.
+ *     @type bool   $remove_links      If true, URLs will be stripped. Default: false.
+ *                                     Only applicable if $html is set to false.
  * }
  * @return string Trimmed string.
  */
@@ -818,7 +824,9 @@
 		'ending'            => __( ' [&hellip;]', 'buddypress' ),
 		'exact'             => false,
 		'html'              => true,
-		'filter_shortcodes' => $filter_shortcodes_default
+		'filter_shortcodes' => $filter_shortcodes_default,
+		'strip_tags'        => false,
+		'remove_links'      => false,
 	), 'create_excerpt' );
 
 	// Save the original text, to be passed along to the filter.
@@ -904,8 +912,28 @@
 			}
 		}
 	} else {
+		// Strip HTML tags if necessary.
+		if ( ! empty( $r['strip_tags'] ) ) {
+			$text = strip_tags( $text );
+		}
+
+		// Remove links if necessary.
+		if ( ! empty( $r['remove_links'] ) ) {
+			$text = preg_replace( '#^\s*(https?://[^\s"]+)\s*$#im', '', $text );
+		}
+
 		if ( mb_strlen( $text ) <= $length ) {
-			return $text;
+			/**
+			 * Filters the final generated excerpt.
+			 *
+			 * @since 1.1.0
+			 *
+			 * @param string $truncate      Generated excerpt.
+			 * @param string $original_text Original text provided.
+			 * @param int    $length        Length of returned string, including ellipsis.
+			 * @param array  $options       Array of HTML attributes and options.
+			 */
+			return apply_filters( 'bp_create_excerpt', $text, $original_text, $length, $options );
 		} else {
 			$truncate = mb_substr( $text, 0, $length - mb_strlen( $ending ) );
 		}
Index: src/bp-core/bp-core-theme-compatibility.php
===================================================================
--- src/bp-core/bp-core-theme-compatibility.php
+++ src/bp-core/bp-core-theme-compatibility.php
@@ -547,6 +547,55 @@
 }
 
 /**
+ * Create a dummy WP_Post object.
+ *
+ * @since 2.6.0
+ *
+ * @param  array $args Array of optional arguments. Arguments parallel the properties
+ *                    of {@link WP_Post}; see that class for more details.
+ * @return WP_Post
+ */
+function bp_theme_compat_create_dummy_post( $args = array() ) {
+	$args = wp_parse_args( $args, array(
+		'ID'                    => -9999,
+		'post_status'           => 'public',
+		'post_author'           => 0,
+		'post_parent'           => 0,
+		'post_type'             => 'page',
+		'post_date'             => 0,
+		'post_date_gmt'         => 0,
+		'post_modified'         => 0,
+		'post_modified_gmt'     => 0,
+		'post_content'          => '',
+		'post_title'            => '',
+		'post_excerpt'          => '',
+		'post_content_filtered' => '',
+		'post_mime_type'        => '',
+		'post_password'         => '',
+		'post_name'             => '',
+		'guid'                  => '',
+		'menu_order'            => 0,
+		'pinged'                => '',
+		'to_ping'               => '',
+		'ping_status'           => '',
+		'comment_status'        => 'closed',
+		'comment_count'         => 0,
+		'filter'                => 'raw',
+
+		'is_404'                => false,
+		'is_page'               => false,
+		'is_single'             => false,
+		'is_archive'            => false,
+		'is_tax'                => false,
+	) );
+
+	// Create the dummy post.
+	$post = new WP_Post( (object) $args );
+
+	return $post;
+}
+
+/**
  * Populate various WordPress globals with dummy data to prevent errors.
  *
  * This dummy data is necessary because theme compatibility essentially fakes
@@ -567,7 +616,7 @@
 
 	// Switch defaults if post is set.
 	if ( isset( $wp_query->post ) ) {
-		$dummy = wp_parse_args( $args, array(
+		$args = wp_parse_args( $args, array(
 			'ID'                    => $wp_query->post->ID,
 			'post_status'           => $wp_query->post->post_status,
 			'post_author'           => $wp_query->post->post_author,
@@ -592,55 +641,16 @@
 			'comment_status'        => $wp_query->post->comment_status,
 			'comment_count'         => $wp_query->post->comment_count,
 			'filter'                => $wp_query->post->filter,
-
-			'is_404'                => false,
-			'is_page'               => false,
-			'is_single'             => false,
-			'is_archive'            => false,
-			'is_tax'                => false,
-		) );
-	} else {
-		$dummy = wp_parse_args( $args, array(
-			'ID'                    => -9999,
-			'post_status'           => 'public',
-			'post_author'           => 0,
-			'post_parent'           => 0,
-			'post_type'             => 'page',
-			'post_date'             => 0,
-			'post_date_gmt'         => 0,
-			'post_modified'         => 0,
-			'post_modified_gmt'     => 0,
-			'post_content'          => '',
-			'post_title'            => '',
-			'post_excerpt'          => '',
-			'post_content_filtered' => '',
-			'post_mime_type'        => '',
-			'post_password'         => '',
-			'post_name'             => '',
-			'guid'                  => '',
-			'menu_order'            => 0,
-			'pinged'                => '',
-			'to_ping'               => '',
-			'ping_status'           => '',
-			'comment_status'        => 'closed',
-			'comment_count'         => 0,
-			'filter'                => 'raw',
-
-			'is_404'                => false,
-			'is_page'               => false,
-			'is_single'             => false,
-			'is_archive'            => false,
-			'is_tax'                => false,
 		) );
 	}
 
 	// Bail if dummy post is empty.
-	if ( empty( $dummy ) ) {
+	if ( empty( $args ) ) {
 		return;
 	}
 
 	// Set the $post global.
-	$post = new WP_Post( (object) $dummy );
+	$post = bp_theme_compat_create_dummy_post( $args );
 
 	// Copy the new post global into the main $wp_query.
 	$wp_query->post       = $post;
@@ -648,14 +658,11 @@
 
 	// Prevent comments form from appearing.
 	$wp_query->post_count = 1;
-	$wp_query->is_404     = $dummy['is_404'];
-	$wp_query->is_page    = $dummy['is_page'];
-	$wp_query->is_single  = $dummy['is_single'];
-	$wp_query->is_archive = $dummy['is_archive'];
-	$wp_query->is_tax     = $dummy['is_tax'];
-
-	// Clean up the dummy post.
-	unset( $dummy );
+	$wp_query->is_404     = $post->is_404;
+	$wp_query->is_page    = $post->is_page;
+	$wp_query->is_single  = $post->is_single;
+	$wp_query->is_archive = $post->is_archive;
+	$wp_query->is_tax     = $post->is_tax;
 
 	/**
 	 * Force the header back to 200 status if not a deliberate 404
@@ -678,28 +685,14 @@
  *
  * @since 1.7.0
  *
- * @uses bp_is_single_user() To check if page is single user.
- * @uses bp_get_single_user_template() To get user template.
- * @uses bp_is_single_user_edit() To check if page is single user edit.
- * @uses bp_get_single_user_edit_template() To get user edit template.
- * @uses bp_is_single_view() To check if page is single view.
- * @uses bp_get_single_view_template() To get view template.
- * @uses bp_is_forum_edit() To check if page is forum edit.
- * @uses bp_get_forum_edit_template() To get forum edit template.
- * @uses bp_is_topic_merge() To check if page is topic merge.
- * @uses bp_get_topic_merge_template() To get topic merge template.
- * @uses bp_is_topic_split() To check if page is topic split.
- * @uses bp_get_topic_split_template() To get topic split template.
- * @uses bp_is_topic_edit() To check if page is topic edit.
- * @uses bp_get_topic_edit_template() To get topic edit template.
- * @uses bp_is_reply_edit() To check if page is reply edit.
- * @uses bp_get_reply_edit_template() To get reply edit template.
- * @uses bp_set_theme_compat_template() To set the global theme compat template.
- *
  * @param string $template Template name.
  * @return string $template Template name.
  */
 function bp_template_include_theme_compat( $template = '' ) {
+	// If embed template, bail.
+	if ( true === function_exists( 'is_embed' ) && is_embed() ) {
+		return $template;
+	}
 
 	// If the current theme doesn't need theme compat, bail at this point.
 	if ( ! bp_use_theme_compat_with_current_theme() ) {
Index: src/bp-core/classes/class-bp-admin.php
===================================================================
--- src/bp-core/classes/class-bp-admin.php
+++ src/bp-core/classes/class-bp-admin.php
@@ -836,6 +836,7 @@
 				<a href="https://bbpress.org">bbPress</a>,
 				<a href="https://github.com/ichord/Caret.js">Caret.js</a>,
 				<a href="http://tedgoas.github.io/Cerberus/">Cerberus</a>,
+				<a href="http://ionicons.com/">Ionicons</a>,
 				<a href="https://github.com/carhartl/jquery-cookie">jquery.cookie</a>,
 				<a href="https://www.mediawiki.org/wiki/MediaWiki">MediaWiki</a>,
 				<a href="https://wordpress.org">WordPress</a>.
Index: src/bp-core/classes/class-bp-oembed-component.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-core/classes/class-bp-oembed-component.php
@@ -0,0 +1,579 @@
+<?php
+/**
+ * Core component classes.
+ *
+ * @package BuddyPress
+ * @subpackage Core
+ */
+
+// Exit if accessed directly.
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * API for responding and returning a custom oEmbed request.
+ *
+ * @since 2.6.0
+ */
+abstract class BP_oEmbed_Component {
+
+	/** START PROPERTIES ****************************************************/
+
+	/**
+	 * (required) The slug endpoint.
+	 *
+	 * Should be your component id.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @var string
+	 */
+	public $slug_endpoint = '';
+
+	/** END PROPERTIES ******************************************************/
+
+	/**
+	 * Constructor.
+	 */
+	final public function __construct() {
+		$this->setup_properties();
+
+		// Some rudimentary logic checking.
+		if ( empty( $this->slug_endpoint ) ) {
+			$class = get_class( $this );
+			throw new LogicException( $class . ' class must define $slug_endpoint property' );
+		}
+
+		$this->setup_hooks();
+		$this->custom_hooks();
+	}
+
+	/** REQUIRED METHODS ****************************************************/
+
+	/**
+	 * Add content for your oEmbed response here.
+	 *
+	 * @since 2.6.0
+	 */
+	abstract protected function content();
+
+	/**
+	 * Add a check for when you are on the page you want to oEmbed.
+	 *
+	 * You'll want to return a boolean here. eg. bp_is_single_activity().
+	 *
+	 * @since 2.6.0
+	 *
+	 * @return bool
+	 */
+	abstract protected function is_page();
+
+	/**
+	 * Validate the URL to see if it matches your item ID.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @return int Your item ID
+	 */
+	abstract protected function validate_url_to_item_id( $url );
+
+	/**
+	 * Set the oEmbed response data.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  int   $item_id Your item ID to do checks against.
+	 * @return array Should contain 'user_id', 'content', 'title', 'author_url' as array keys.
+	 *               'author_url' is optional; the rest are required.
+	 */
+	abstract protected function set_oembed_response_data( $item_id );
+
+	/**
+	 * Sets the fallback HTML for the oEmbed response.
+	 *
+	 * In a WordPress oEmbed item, the fallback HTML is a <blockquote>.  This is
+	 * usually hidden after the <iframe> is loaded.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  int    $item_id Your item ID to do checks against.
+	 * @return string Fallback HTML you want to output.
+	 */
+	abstract protected function set_fallback_html( $item_id );
+
+	/** OPTIONAL METHODS ****************************************************/
+
+	/**
+	 * If your oEmbed endpoint requires additional arguments, set them here.
+	 *
+	 * @see register_rest_route() View the $args parameter for more info.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @return array
+	 */
+	protected function set_route_args() {
+		return array();
+	}
+
+	/**
+	 * Set the iframe title.
+	 *
+	 * If not set, this will fallback to WP's 'Embedded WordPress Post'.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param int $item_id The item ID to do checks for.
+	 * @return string
+	 */
+	protected function set_iframe_title( $item_id ) {}
+
+	/**
+	 * Do what you need to do here to initialize any custom hooks.
+	 *
+	 * @since 2.6.0
+	 */
+	protected function custom_hooks() {}
+
+	/**
+	 * Set permalink for oEmbed link discovery.
+	 *
+	 * This method will be called on the page we want to oEmbed.  In most cases,
+	 * you will not need to override this method.  However, if you need to, do
+	 * override in your extended class.
+	 *
+	 * @since 2.6.0
+	 */
+	protected function set_permalink() {
+		$url = bp_get_requested_url();
+
+		// Remove querystring from bp_get_requested_url()
+		if ( false !== strpos( bp_get_requested_url(), '?' ) ) {
+			$url = substr( bp_get_requested_url(), 0, strpos( bp_get_requested_url(), '?' ) );
+		}
+
+		return $url;
+	}
+
+	/** HELPERS *************************************************************/
+
+	/**
+	 * Get the item ID when filtering the oEmbed HTML.
+	 *
+	 * Should only be used during the 'embed_html' hook.
+	 *
+	 * @since 2.6.0
+	 */
+	protected function get_item_id() {
+		return $this->is_page() ? $this->validate_url_to_item_id( $this->set_permalink() ) : buddypress()->{$this->slug_endpoint}->embedid_in_progress;
+	}
+
+	/** SET UP **************************************************************/
+
+	/**
+	 * Set up properties.
+	 *
+	 * @since 2.6.0
+	 */
+	protected function setup_properties() {
+		$this->slug_endpoint = sanitize_title( $this->slug_endpoint );
+	}
+
+	/**
+	 * Hooks! We do the dirty work here, so you don't have to! :)
+	 *
+	 * More hooks are available in the setup_template_parts() method.
+	 *
+	 * @since 2.6.0
+	 */
+	protected function setup_hooks() {
+		add_action( 'rest_api_init',    array( $this, 'register_route' ) );
+		add_action( 'bp_embed_content', array( $this, 'inject_content' ) );
+
+		add_filter( 'embed_template', array( $this, 'setup_template_parts' ) );
+		add_filter( 'post_embed_url', array( $this, 'filter_embed_url' ) );
+		add_filter( 'embed_html',     array( $this, 'filter_embed_html' ) );
+		add_filter( 'oembed_discovery_links', array( $this, 'add_oembed_discovery_links' ) );
+		add_filter( 'rest_pre_serve_request', array( $this, 'oembed_xml_request' ), 20, 4 );
+	}
+
+	/** HOOKS ***************************************************************/
+
+	/**
+	 * Register the oEmbed REST API route.
+	 *
+	 * @since 2.6.0
+	 */
+	public function register_route() {
+		/** This filter is documented in wp-includes/class-wp-oembed-controller.php */
+		$maxwidth = apply_filters( 'oembed_default_width', 600 );
+
+		// Required arguments.
+		$args = array(
+			'url'      => array(
+				'required'          => true,
+				'sanitize_callback' => 'esc_url_raw',
+			),
+			'format'   => array(
+				'default'           => 'json',
+				'sanitize_callback' => 'wp_oembed_ensure_format',
+			),
+			'maxwidth' => array(
+				'default'           => $maxwidth,
+				'sanitize_callback' => 'absint',
+			)
+		);
+
+		// Merge custom arguments here.
+		$args = $args + (array) $this->set_route_args();
+
+		register_rest_route( 'oembed/1.0', "/embed/{$this->slug_endpoint}", array(
+			array(
+				'methods'  => WP_REST_Server::READABLE,
+				'callback' => array( $this, 'get_item' ),
+				'args'     => $args
+			),
+		) );
+	}
+
+	/**
+	 * Set up custom embed template parts for BuddyPress use.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  string $template File path to current embed template.
+	 * @return string
+	 */
+	public function setup_template_parts( $template ) {
+		// Determine if we're on our BP page.
+		if ( ! $this->is_page() || is_404() ) {
+			return $template;
+		}
+
+		// Set up some BP-specific embed template overrides.
+		add_action( 'get_template_part_embed', array( $this, 'content_buffer_start' ), -999, 2 );
+		add_action( 'get_footer',              array( $this, 'content_buffer_end' ), -999 );
+
+		// Return the original WP embed template.
+		return $template;
+	}
+
+	/**
+	 * Start object buffer.
+	 *
+	 * We're going to override WP's get_template_part( 'embed, 'content' ) call
+	 * and inject our own template for BuddyPress use.
+	 *
+	 * @since 2.6.0
+	 */
+	public function content_buffer_start( $slug, $name ) {
+		if ( 'embed' !== $slug || 'content' !== $name ) {
+			return;
+		}
+
+		// Start the buffer to wipe out get_template_part( 'embed, 'content' ).
+		ob_start();
+	}
+
+	/**
+	 * End object buffer.
+	 *
+	 * We're going to override WP's get_template_part( 'embed, 'content' ) call
+	 * and inject our own template for BuddyPress use.
+	 *
+	 * @since 2.6.0
+	 */
+	public function content_buffer_end( $name ) {
+		if ( 'embed' !== $name || is_404() ) {
+			return;
+		}
+
+		// Wipe out get_template_part( 'embed, 'content' ).
+		ob_end_clean();
+
+		// Start our custom BuddyPress embed template!
+		echo '<div ';
+		post_class( 'wp-embed' );
+		echo '>';
+
+		// Template part for our embed header.
+		bp_get_asset_template_part( 'embeds/header', bp_current_component() );
+
+		/**
+		 * Inject BuddyPress embed content on this hook.
+		 *
+		 * You shouldn't really need to use this if you extend the
+		 * {@link BP_oEmbed_Component} class.
+		 *
+		 * @since 2.6.0
+		 */
+		do_action( 'bp_embed_content' );
+
+		// Template part for our embed footer.
+		bp_get_asset_template_part( 'embeds/footer', bp_current_component() );
+
+		echo '</div>';
+	}
+
+	/**
+	 * Adds oEmbed discovery links on single activity pages.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  string $retval Current discovery links.
+	 * @return string
+	 */
+	public function add_oembed_discovery_links( $retval ) {
+		if ( ! $this->is_page() ) {
+			return $retval;
+		}
+
+		$permalink = $this->set_permalink();
+		if ( empty( $permalink ) ) {
+			return $retval;
+		}
+
+		add_filter( 'rest_url' , array( $this, 'filter_rest_url' ) );
+
+		$retval = '<link rel="alternate" type="application/json+oembed" href="' . esc_url( get_oembed_endpoint_url( $permalink ) ) . '" />' . "\n";
+
+		if ( class_exists( 'SimpleXMLElement' ) ) {
+			$retval .= '<link rel="alternate" type="text/xml+oembed" href="' . esc_url( get_oembed_endpoint_url( $permalink, 'xml' ) ) . '" />' . "\n";
+		}
+
+		remove_filter( 'rest_url' , array( $this, 'filter_rest_url' ) );
+
+		return $retval;
+	}
+
+	/**
+	 * Callback for the API endpoint.
+	 *
+	 * Returns the JSON object for the item.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  WP_REST_Request $request Full data about the request.
+	 * @return WP_Error|array oEmbed response data or WP_Error on failure.
+	 */
+	public function get_item( $request ) {
+		$url = $request['url'];
+
+		$data = false;
+
+		$item_id = (int) $this->validate_url_to_item_id( $url );
+
+		if ( ! empty( $item_id ) ) {
+			$item = $this->set_oembed_response_data( $item_id );
+
+			// Create dummy post to piggyback off of get_oembed_response_data()
+			$post = bp_theme_compat_create_dummy_post( array(
+				'post_author'  => $item['user_id'],
+				'post_title'   => $item['title'],
+				'post_content' => $item['content'],
+
+				// This passes the get_oembed_response_data() check.
+				'post_status'  => 'publish'
+			) );
+
+			// Add markers to tell that we're embedding a single activity.
+			// This is needed for various oEmbed response data filtering.
+			if ( empty( buddypress()->{$this->slug_endpoint} ) ) {
+				buddypress()->{$this->slug_endpoint} = new stdClass;
+			}
+			buddypress()->{$this->slug_endpoint}->embedurl_in_progress = $url;
+			buddypress()->{$this->slug_endpoint}->embedid_in_progress  = $item_id;
+
+			// Save custom route args as well.
+			$custom_args = array_keys( (array) $this->set_route_args() );
+			if ( ! empty( $custom_args ) ) {
+				buddypress()->{$this->slug_endpoint}->embedargs_in_progress = array();
+
+				foreach( $custom_args as $arg ) {
+					if ( isset( $request[ $arg ] ) ) {
+						buddypress()->{$this->slug_endpoint}->embedargs_in_progress[ $arg ] = $request[ $arg ];
+					}
+				}
+			}
+
+			// Use WP's oEmbed response data function.
+			$data = get_oembed_response_data( $post, $request['maxwidth'] );
+
+			// Set custom 'author_url' if we have one.
+			if ( ! empty( $item['author_url'] ) ) {
+				$data['author_url'] = $item['author_url'];
+			}
+		}
+
+		if ( ! $data ) {
+			return new WP_Error( 'oembed_invalid_url', get_status_header_desc( 404 ), array( 'status' => 404 ) );
+		}
+
+		return $data;
+	}
+
+	/**
+	 * If oEmbed request wants XML, return XML instead of JSON.
+	 *
+	 * Basically a copy of {@link _oembed_rest_pre_serve_request()}. Unfortunate
+	 * that we have to duplicate this just for a URL check.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param bool                      $served  Whether the request has already been served.
+	 * @param WP_HTTP_ResponseInterface $result  Result to send to the client. Usually a WP_REST_Response.
+	 * @param WP_REST_Request           $request Request used to generate the response.
+	 * @param WP_REST_Server            $server  Server instance.
+	 * @return bool
+	 */
+	public function oembed_xml_request( $served, $result, $request, $server ) {
+		$params = $request->get_params();
+
+		if ( ! isset( $params['format'] ) || 'xml' !== $params['format'] ) {
+			return $served;
+		}
+
+		// Validate URL against our oEmbed endpoint. If not valid, bail.
+		// This is our mod to _oembed_rest_pre_serve_request().
+		$query_params = $request->get_query_params();
+		if ( false === $this->validate_url_to_item_id( $query_params['url'] ) ) {
+			return $served;
+		}
+
+		// Embed links inside the request.
+		$data = $server->response_to_data( $result, false );
+
+		if ( ! class_exists( 'SimpleXMLElement' ) ) {
+			status_header( 501 );
+			die( get_status_header_desc( 501 ) );
+		}
+
+		$result = _oembed_create_xml( $data );
+
+		// Bail if there's no XML.
+		if ( ! $result ) {
+			status_header( 501 );
+			return get_status_header_desc( 501 );
+		}
+
+		if ( ! headers_sent() ) {
+			$server->send_header( 'Content-Type', 'text/xml; charset=' . get_option( 'blog_charset' ) );
+		}
+
+		echo $result;
+
+		return true;
+	}
+
+	/**
+	 * Pass our BuddyPress activity permalink for embedding.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @see bp_activity_embed_rest_route_callback()
+	 *
+	 * @param  string $retval Current embed URL
+	 * @return string
+	 */
+	public function filter_embed_url( $retval ) {
+		if ( false === isset( buddypress()->{$this->slug_endpoint}->embedurl_in_progress ) && ! $this->is_page() ) {
+			return $retval;
+		}
+
+		$url = $this->is_page() ? $this->set_permalink() : buddypress()->{$this->slug_endpoint}->embedurl_in_progress;
+		$url = trailingslashit( $url );
+
+		// This is for the 'WordPress Embed' block
+		// @see bp_activity_embed_comments_button()
+		if ( 'the_permalink' !== current_filter() ) {
+			$url = add_query_arg( 'embed', 'true', trailingslashit( $url ) );
+
+			// Add custom route args to iframe.
+			if ( ! empty( buddypress()->{$this->slug_endpoint}->embedargs_in_progress ) ) {
+				foreach( buddypress()->{$this->slug_endpoint}->embedargs_in_progress as $key => $value ) {
+					$url = add_query_arg( $key, $value, $url );
+				}
+			}
+		}
+
+		return $url;
+	}
+
+	/**
+	 * Filters the embed HTML for our BP oEmbed endpoint.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @param  string $retval Current embed HTML
+	 * @return string
+	 */
+	public function filter_embed_html( $retval ) {
+		if ( false === isset( buddypress()->{$this->slug_endpoint}->embedurl_in_progress ) && ! $this->is_page() ) {
+			return $retval;
+		}
+
+		$url = $this->set_permalink();
+
+		$item_id = $this->is_page() ? $this->validate_url_to_item_id( $url ) : buddypress()->{$this->slug_endpoint}->embedid_in_progress;
+
+		// Change 'Embedded WordPress Post' to custom title.
+		$custom_title = $this->set_iframe_title( $item_id );
+		if ( ! empty( $custom_title ) ) {
+			$title_pos = strpos( $retval, 'title=' ) + 7;
+			$title_end_pos = strpos( $retval, '"', $title_pos );
+
+			$retval = substr_replace( $retval, esc_attr( $custom_title ), $title_pos, $title_end_pos - $title_pos );
+		}
+
+		// Add 'max-width' CSS attribute to IFRAME.
+		// This will make our oEmbeds responsive.
+		if ( false === strpos( $retval, 'style="max-width' ) ) {
+			$retval = str_replace( '<iframe', '<iframe style="max-width:100%"', $retval );
+		}
+
+		// Remove default <blockquote>
+		$retval = substr( $retval, strpos( $retval, '</blockquote>' ) + 13 );
+
+		// Set up new fallback HTML
+		// @todo Maybe use KSES?
+		$fallback_html = $this->set_fallback_html( $item_id );
+
+		/**
+		 * Dynamic filter to return BP oEmbed HTML.
+		 *
+		 * @since 2.6.0
+		 *
+		 * @var string $retval
+		 */
+		return apply_filters( "bp_{$this->slug_endpoint}_embed_html", $fallback_html . $retval );
+	}
+
+	/**
+	 * Append our custom slug endpoint to oEmbed endpoint URL.
+	 *
+	 * Meant to be used as a filter on 'rest_url' before any call to
+	 * {@link get_oembed_endpoint_url()} is used.
+	 *
+	 * @since 2.6.0
+	 *
+	 * @see add_oembed_discovery_links()
+	 *
+	 * @param  string $retval Current oEmbed endpoint URL
+	 * @return string
+	 */
+	function filter_rest_url( $retval = '' ) {
+		return $retval . "/{$this->slug_endpoint}";
+	}
+
+	/**
+	 * Inject content into the embed template.
+	 *
+	 * @since 2.6.0
+	 */
+	public function inject_content() {
+		if ( ! $this->is_page() ) {
+			return;
+		}
+
+		$this->content();
+	}
+}
\ No newline at end of file
Index: src/bp-templates/bp-legacy/buddypress/assets/embeds/activity.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-templates/bp-legacy/buddypress/assets/embeds/activity.php
@@ -0,0 +1,12 @@
+
+		<?php if ( bp_activity_embed_has_activity( bp_current_action() ) ) : ?>
+
+			<?php while ( bp_activities() ) : bp_the_activity(); ?>
+				<div class="bp-embed-excerpt"><?php bp_activity_embed_excerpt(); ?></div>
+
+				<?php bp_activity_embed_media(); ?>
+
+			<?php endwhile; ?>
+
+		<?php endif; ?>
+
Index: src/bp-templates/bp-legacy/buddypress/assets/embeds/css-activity.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-templates/bp-legacy/buddypress/assets/embeds/css-activity.php
@@ -0,0 +1,113 @@
+#bp-embed-header:after {
+	clear: both;
+	content: "";
+	display: table;
+	margin-bottom: 1em;
+}
+
+.bp-embed-avatar {
+	float: left;
+	margin: 0 .75em 0 0;
+}
+
+p.bp-embed-activity-action {
+	font-size: 15px;
+	margin-bottom: 0;
+}
+
+p.bp-embed-activity-action a:first-child {
+	color: #32373c;
+	font-weight: bold;
+}
+
+p.bp-embed-activity-action img.avatar {
+	padding: 0 4px 0 3px;
+	vertical-align: text-bottom;
+}
+
+.bp-embed-excerpt {
+	margin-bottom: 1em;
+}
+
+.bp-embed-excerpt a {
+	color: #21759b;
+	display: inline-block;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	vertical-align: top;
+	white-space: nowrap;
+	max-width: 250px;
+}
+
+.activity-read-more {
+	margin-left: .5em;
+}
+
+.activity-read-more a {
+	color: #b4b9be;
+}
+
+.wp-embed-footer {
+	margin-top: 20px;
+}
+
+span.bp-embed-timestamp {
+	font-size: .9em;
+}
+
+video {
+	width: 100%;
+	height: auto;
+}
+
+.bp-activity-embed-display-media {
+	border: 1px solid #ccc;
+	border-radius: 6px;
+}
+
+.bp-activity-embed-display-media,
+.bp-activity-embed-display-media .thumb,
+.bp-activity-embed-display-media .thumb img {
+	width: 100%;
+}
+
+.bp-activity-embed-display-media .thumb {
+	position: relative;
+}
+
+.bp-activity-embed-display-media .caption {
+	padding: .2em .5em .5em .5em;
+}
+
+a.play-btn {
+	background: rgba(0, 0, 0, 0.75);
+	border-radius: 50%;
+	height: 50px;
+	left: 50%;
+	margin: 0;
+	padding: 1em;
+	position: absolute;
+	text-indent: 0.25em;
+	top: 50%;
+	transform: translateY(-50%) translateX(-50%);
+	-webkit-transform: translateY(-50%) translateX(-50%);
+	transition: all 0.2s ease-out;
+	width: 50px;
+}
+
+a.play-btn:hover {
+	background: rgba(0, 0, 0, 0.95);
+	transform: translateY(-50%) translateX(-50%) scale(1.05);
+	-webkit-transform: translateY(-50%) translateX(-50%) scale(1.05);
+	transition: all 0.2s ease-out;
+}
+
+.bp-activity-embed-display-media .thumb svg {
+	fill: #fff;
+	overflow: hidden;
+}
+
+.bp-activity-embed-display-media .caption-description {
+	font-size: 90%;
+	margin: .4em 0;
+}
\ No newline at end of file
Index: src/bp-templates/bp-legacy/buddypress/assets/embeds/footer.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-templates/bp-legacy/buddypress/assets/embeds/footer.php
@@ -0,0 +1,9 @@
+			<div class="wp-embed-footer">
+				<?php the_embed_site_title() ?>
+
+				<div class="wp-embed-meta">
+					<?php
+					/** This action is documented in wp-includes/theme-compat/embed-content.php */
+					do_action( 'embed_content_meta'); ?>
+				</div>
+			</div>
\ No newline at end of file
Index: src/bp-templates/bp-legacy/buddypress/assets/embeds/header-activity.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-templates/bp-legacy/buddypress/assets/embeds/header-activity.php
@@ -0,0 +1,26 @@
+
+		<div id="bp-embed-header">
+			<div class="bp-embed-avatar">
+				<a href="<?php bp_displayed_user_link(); ?>">
+					<?php bp_displayed_user_avatar( 'type=thumb&width=45&height=45' ); ?>
+				</a>
+			</div>
+
+			<?php if ( bp_activity_embed_has_activity( bp_current_action() ) ) : ?>
+
+				<?php while ( bp_activities() ) : bp_the_activity(); ?>
+					<p class="bp-embed-activity-action">
+						<?php bp_activity_action( array( 'no_timestamp' => true ) ); ?>
+					</p>
+				<?php endwhile; ?>
+
+			<?php endif; ?>
+
+			<p class="bp-embed-header-meta">
+				<?php if ( bp_is_active( 'activity' ) && bp_activity_do_mentions() ) : ?>
+					<span class="bp-embed-mentionname">@<?php bp_displayed_user_mentionname(); ?> &middot; </span>
+				<?php endif; ?>
+
+				<span class="bp-embed-timestamp"><a href="<?php bp_activity_thread_permalink(); ?>"><?php echo date_i18n( get_option( 'time_format' ) . ' - ' . get_option( 'date_format' ), strtotime( bp_get_activity_date_recorded() ) ); ?></a></span>
+			</p>
+		</div>
Index: src/bp-templates/bp-legacy/buddypress/assets/embeds/header.php
new file mode 100644
===================================================================
--- /dev/null
+++ src/bp-templates/bp-legacy/buddypress/assets/embeds/header.php
@@ -0,0 +1,18 @@
+
+		<div id="bp-embed-header">
+			<div class="bp-embed-avatar">
+				<a href="<?php bp_displayed_user_link(); ?>">
+					<?php bp_displayed_user_avatar( 'type=thumb&width=36&height=36' ); ?>
+				</a>
+			</div>
+
+			<p class="wp-embed-heading">
+				<a href="<?php bp_displayed_user_link(); ?>">
+					<?php bp_displayed_user_fullname(); ?>
+				</a>
+			</p>
+
+			<?php if ( bp_is_active( 'activity' ) && bp_activity_do_mentions() ) : ?>
+				<p class="bp-embed-mentionname">@<?php bp_displayed_user_mentionname(); ?></p>
+			<?php endif; ?>
+		</div>
