Integrating Hippo CMS with Brightcove Video Cloud
Mike Marmar Melvin Monteiro
2015-01-08
With video consumption at all-time high, every B2B and B2C brand is looking to manage and distribute video content at speed and at scale. A recent study by Google and Millward Brown Digital showed that 70% of B2B buyers and researchers are watching videos throughout their path to purchase. In the B2C space, 6 in 10 adults say they watch videos when they visit a brand website. So if you are'nt taking video seriously, you're about to miss out on a major opportunity. One of the most critical integrations to enable effective management of video is the connection between your CMS and your online video management platform (OVMP) like Brightcove, Vimeo and Youtube. OVMPs were created to ease the effort of video management and distribution. They are usually SaaS-based and provide video file transcoding, device detection, video hosting and CDN service for distribution and syndication, in-depth analytics and player customizations. One of the benefits of using OVMPs is that the many large video files are stored externally of your CMS, reducing clutter and any possible performance impact. We've selected BrightCove for this demonstration.
Brightcove Video Cloud
Brightcove is an enterprise-class online video management platform with thousands of clients and a good share of the market. It has a robust API to enable the type of integration we have outlined below. Many organizations may have several OVMPs and may also store some video within internal/CMS repositories. A client-specific implementation of such an integration should allow the content contributor/author to select video's from any number of sources.To ease the burden of managing video assets within Hippo, we built a deep integration plugin that allows a content author to browse and search Brightcove with an extension of the native Hippo file picker. By taking advantage of Hippo's flexibilty, we can provide a seamless integration with Brightcove such that videos can remain in Brightcove but still become available to Hippo without importing the actual video content. This means that videos (and their metadata) are still managed centrally in Brightcove and distributed by the Brightcove CDN. The benefits of building the plugin as an extension to the native functionality is manifold. First, it provides content authors with a seamless and familiar interface. Second, it reduces developer burden as much of the heavy lifting is done by existing Hippo functionality. Most importantly, it means that it plays nicely with existing functionality such as targeting. We will see in detail how this plays out in the following walkthrough and deep dive into the code.
Walkthrough
The following walkthrough is built on the Go Green Demo application. Check out the video walkthrough of the implementaion on our website.
Authoring : There are two places that videos can be added by a content author:
1. The video Component :
- Here, we can drop a Brightcove video component directly on a page. First, open the English Website Channel:
- This takes us to the homepage. Click "Edit" to open the page editor, and click "Components" to select the Brightcove video component. Drag and drop the Brightcove Video component in the hero section of the page:
- After dropping the component on the page, click it to open the component properties editor:
- To select a video, click the search icon to open the video browser. This opens a new picker showing all the Brightcove videos in our account:
- As you can see, the picker has video previews, metadata, sorting, pagination, filtering and search. To preview a video, click its thumbnail. To select a video, click the "select" button. This will embed the video in the page. After selecting the video, the Brightcove video id is dropped into the component configuration. Because we simply reference the id, we are able to use standard functionality such as targeting:
2. The Document :
- A video can also be added directly to a document. To see this in action, open the Content browser and browse to Hippo Go Green -> Products -> Food -> 2011 -> Lentil and Mushroom Stew:
- Click "Edit" and scroll down to the "Browse Video" section. Here, click the "Browse" button to open the same video picker:
- Once a video has been selected, we see that the metadata from Brightcove is pulled in, and can be edited:
Administration
- The configuration for the document-based video browser is located in the JCR store at /hippo:namespaces/hippogogreen/videoCompound/editor:templates/_default_/videoId:
- Here you can select the Plugin implementation class as well as the video provider. This allows for future implementation of other providers (YouTube, Vimeo, etc).
- Configuration for the CMS video browser is located in the JCR store at /hippo:configuration/hippo:frontend/cms/cms-pickers/videos/brightcoveVideo:
- This allows for configuration of the browser dialog dimensions, title, pagination, video preview, and API tokens.
Implementation details
The Brightcove video browser is implemented as a Hippo Plugin. The steps for creating the plugin are as follows:
Step 1: Create the plugin entrypoint
The entrypoint class is called VideoFieldPlugin:
public class VideoFieldPlugin extends RenderPlugin {
public VideoFieldPlugin(IPluginContext context, IPluginConfig config) {
super(context, config);
IPluginConfigService pluginConfigService = context.getService(IPluginConfigService.class.getName(), IPluginConfigService.class);
//Get provider id
this.providerId = config.getString("provider.id");
if (StringUtils.isEmpty(providerId)) {
throw new WicketRuntimeException("'provider.id' needs to be set");
}
try {
VideoProviderFactory videoProviderFactory = new VideoProviderFactory(pluginConfigService,providerId);
videoProvider = videoProviderFactory.getVideoProvider();
videoConfig = videoProviderFactory.getVideoConfig();
projectNamespace = this.videoConfig.getString("field.namespace");
currentNodeModel = (JcrNodeModel) getModel();
valueModel = getValueModel(this.currentNodeModel);
videoPanel = videoProviderFactory.getVideoPanel(valueModel);
}
catch(Exception ex) {
throw new WicketRuntimeException("unable to get video provider");
}
//...Truncated for brevity...
/* Mode for VIEW */
if (mode.equals(IEditor.Mode.VIEW) || mode.equals(IEditor.Mode.COMPARE)) {
fragment = new Fragment("fragment", "view", this);
fragment.add((Panel)videoPanel);
}
/* Mode for EDIT*/
else if (mode.equals(IEditor.Mode.EDIT)) {
fragment = new Fragment("fragment", "edit", this);
fragment.add(new Component[] { new AjaxLink("select")
{
private static final long serialVersionUID = 1L;
public void onClick(AjaxRequestTarget target) {
dialogService.show(createDialog());
}
}
});
fragment.add((Panel)videoPanel);
}
add(new Component[] { fragment });
setOutputMarkupId(true);
modelChanged();
}
private AbstractDialog createDialog()
{
IChainingModel dialogModel = new IChainingModel()
{
private static final long serialVersionUID = 1L;
public Video getObject() {
return VideoFieldPlugin.this.videoModel.getObject();
}
public void setObject(Video paramT) {
VideoFieldPlugin.this.videoModel.setObject((Video)paramT);
VideoFieldPlugin.this.modelChanged();
}
public void detach() {
if (VideoFieldPlugin.this.videoModel != null) {
VideoFieldPlugin.this.videoModel.detach();
}
}
public IModel getChainedModel() {
return VideoFieldPlugin.this.videoModel;
}
public void setChainedModel(IModel paramIModel) {
throw new UnsupportedOperationException("Value model cannot be changed");
}
};
return new VideoBrowserDialog(getPluginContext(), VideoFieldPlugin.this.videoConfig, dialogModel, VideoFieldPlugin.this.providerId, false);
}
public void onModelChanged()
{
Video v = this.videoModel.getObject();
String videoId = v.getId().toString();
videoPanel.updatePanel(videoId);
Node node = this.currentNodeModel.getNode();
node.setProperty(this.projectNamespace + ":" +VIDEO_ID, videoId );
node.setProperty(this.projectNamespace + ":" +VIDEO_DISPLAY_ID, videoId);
if (StringUtils.isNotEmpty(v.getName()))
node.setProperty(this.projectNamespace + ":" +VIDEO_TITLE, v.getName());
if (StringUtils.isNotEmpty(v.getDuration()))
node.setProperty(this.projectNamespace + ":" +VIDEO_DURATION, v.getDuration());
if (StringUtils.isNotEmpty(v.getShortDescription()))
node.setProperty(this.projectNamespace + ":" +VIDEO_SHORT_DESCRIPTION, v.getShortDescription());
if (StringUtils.isNotEmpty(v.getLongDescription()))
node.setProperty(this.projectNamespace + ":" +VIDEO_LONG_DESCRIPTION, v.getLongDescription());
node.setProperty(this.projectNamespace + ":" +VIDEO_PROVIDER_ID, v.getProviderId());
redraw();
}
}
There are a few key parts to this Plugin. The constructor loads the configuration from the JCR store (provider id, namespace, etc). Then it sets up the views for VIEW mode and for EDIT mode. In VIEW mode, the video panel is displayed which renders the video player. In EDIT mode, a Wicket dialog is rendered for the video browser. onModelChanged is responsible for updating the selected video in the document/component as well as filling out any available metadata fields, and createDialog is responsible for creating the video browser.
Step2 : Set up the Video Provider framework
Next, create the VideoProviderFactory which abstracts the task of choosing the appropriate video provider based on the JCR configuration:
public class VideoProviderFactory {
private IPluginConfig videoConfig;
private VideoProviderType videoProviderType;
public enum VideoProviderType {
BRIGHTCOVE {
public VideoProvider getVideoProvider(IPluginConfig videoConfig)
{
return new BrightcoveVideoProvider(videoConfig);
}
public VideoPanel getVideoPanel(IModel model, IPluginConfig videoConfig)
{
return new BrightcoveVideoPanel("videoPanel", model, videoConfig);
}
},
VIMEO {
public VideoProvider getVideoProvider(IPluginConfig videoConfig)
{
return new VimeoVideoProvider(videoConfig);
}
public VideoPanel getVideoPanel(IModel model, IPluginConfig videoConfig)
{
return new VimeoVideoPanel("videoPanel", model, videoConfig);
}
};
abstract VideoProvider getVideoProvider(IPluginConfig videoConfig);
abstract VideoPanel getVideoPanel(IModel model, IPluginConfig videoConfig);
}
public VideoProviderFactory(IPluginConfigService configService , String providerId ) throws Exception {
if (StringUtils.isEmpty(providerId)) {
throw new Exception("'provider.id' needs to be set");
}
final IClusterConfig template = configService.getCluster("cms-pickers/videos");
if (template == null ) {
throw new Exception("'cms-pickers/videos' cluster not found.");
}
this.videoConfig = template.getPluginConfig(providerId);
if ("brightcoveVideo".equals(providerId)) {
this.videoProviderType = VideoProviderType.BRIGHTCOVE;
}
if ("vimeoVideo".equals(providerId)) {
this.videoProviderType = VideoProviderType.VIMEO;
}
}
public VideoProvider getVideoProvider() {
return videoProviderType.getVideoProvider(videoConfig);
}
public VideoPanel getVideoPanel(IModel model) {
return videoProviderType.getVideoPanel(model, videoConfig);
}
}
Step 3: Implement the video provider
The VideoProviderFactory instantiates and returns the proper implementation of the VideoProvider interface, which is a thin layer that sits on top of the Brightcove (or other provider) API. This allows the Plugin to be provider-agnostic:
public interface VideoProvider {
public Video findVideoById(String videoId) throws Exception;
public Video getCachedVideo(String videoId) throws Exception;
public List findVideos(String searchString, SortParam sort, Integer pageSize , Integer pageNumber) throws Exception;
public List normalizeVideos(List videos);
public long getTotalCount(String searchString) throws Exception;
}
The key step to adding a new video provider (say, Vimeo or Youtube) is to implement this interface. Generally, video provider APIs are a very close match to this interface, so it is straightforward to do so. To see how this works in practice, here is an implementation of the findVideos search method on the Brightcove API:
public List findVideos(String searchString, SortParam sort, Integer pageSize, Integer pageNumber) throws Exception {
try {
ReadApi readApi = new ReadApi();
List any = new ArrayList();
Videos videoList = null;
SortOrderTypeEnum sortOrderType = sort.isAscending() == true ? SortOrderTypeEnum.ASC :SortOrderTypeEnum.DESC;
if (StringUtils.isNotEmpty(searchString)) {
any.add(searchString);
}
else {
any = null;
}
videoList = readApi.SearchVideos(getReadToken(),
null, any, null, false,
getSortTypeEnum(sort),
sortOrderType, pageSize,
pageNumber, videoFieldEnums, null);
List videos = normalizeVideos(videoList);
videoMap.clear();
for (com.kanbansolutions.hippocms.frontend.editor.plugin.videopicker.Video v : videos) {
videoMap.put(v.getId() , v );
}
return videos;
}
catch(BrightcoveException e) {
throw new Exception ("Exception when finding a video", e);
}
}
Here, ReadApi is an instance of the Brightcove API. After minimal processing of the parameters, we can pass them through to the API. normalizeVideos then converts the results (Brightcove Video objects) to the internal representation.
Step 4: Implement supporting classes
In order to render the video picker, implement a custom Wicket dialog, VideoBrowserDialog:
public class VideoBrowserDialog extends Dialog {
private static final JavaScriptResourceReference MYPAGE_JS = new JavaScriptResourceReference(VideoBrowserDialog.class, "VideoBrowserDialog.js");
private static final JavaScriptResourceReference MYPAGE_CSS = new JavaScriptResourceReference(VideoBrowserDialog.class, "VideoBrowserDialog.css");
private AjaxFallbackDefaultDataTable table;
private WebMarkupContainer listContainer;
final List columns = new ArrayList();
private static final String wicketDataTableId = "videoDataTable";
IPluginConfig videoConfig;
VideoProvider videoProvider;
VideoProviderFactory videoProviderFactory;
//default;
Integer pageSize = new Integer(10);
private boolean callbackAjax = false;
public VideoBrowserDialog(final IPluginContext context, final IPluginConfig config, final IModel model, String providerId, boolean callbackAjax) {
//Truncated for brevity...
listContainer = new WebMarkupContainer("wmc");
columns.add(new PropertyColumn(new Model(" "), null ,"thumbnailUrl") {
@Override
public void populateItem(Item item, String componentId, IModel paramIModel) {
String thumbnailUrl = ((DetachableVideoModel)paramIModel).load().getThumbnailUrl();
String videoId = ((DetachableVideoModel)paramIModel).load().getId();
if ("".equals(thumbnailUrl) || null == thumbnailUrl) {
thumbnailUrl = "/cms/wicket/resource/com.kanbansolutions.hippocms.frontend.editor.plugin.videopicker.VideoBrowserDialog/video-icon.png";
}
ThumbnailPanel thumbnail = new ThumbnailPanel(context, config, videoProviderFactory, componentId, "", thumbnailUrl, videoId);
item.add(thumbnail);
item.add(new AttributeAppender("class", new Model("ks-thumbnail"), ""));
}
});
columns.add(new PropertyColumn(new Model("Name"), "name", "name") {
public void populateItem(Item item, String componentId, IModel rowModel) {
super.populateItem(item, componentId, rowModel);
item.add(new AttributeAppender("class", new Model("ks-name"), ""));
}
});
columns.add(new PropertyColumn(new Model("Length"), null , "duration") {
public void populateItem(Item item, String componentId, IModel rowModel) {
super.populateItem(item, componentId, rowModel);
item.add(new AttributeAppender("class", new Model("ks-duration"), ""));
}
});
columns.add(new PropertyColumn(new Model("Last Updated"), "lastModifiedDate", "lastModifiedDate") {
public void populateItem(Item item, String componentId, IModel rowModel) {
super.populateItem(item, componentId, rowModel);
item.add(new AttributeAppender("class", new Model("ks-lastModifiedDate"), ""));
}
});
columns.add(new PropertyColumn(new Model(" "),null,null) {
public void populateItem(Item item, String componentId, IModel paramIModel) {
item.add(new SelectActionPanel( model , componentId, paramIModel, ((DetachableVideoModel)paramIModel).load() ));
item.add(new AttributeAppender("class", new Model("ks-action"), ""));
}
});
SortParam sort = new SortParam("lastModifiedDate", false);
SortableVideoDataProvider videoDataProvider = new SortableVideoDataProvider(videoProvider, sort, null, pageSize);
table = new AjaxFallbackDefaultDataTable(wicketDataTableId, columns, videoDataProvider, pageSize);
listContainer.add(table);
listContainer.add(new SearchPanel("searchPanel"));
listContainer.add( new AjaxLink("allVideos") {
public void onClick(AjaxRequestTarget target) {
//Sort by create
SortParam sort = new SortParam("lastModifiedDate", false);
updateTable(columns,sort,null);
target.add(listContainer);
}
});
listContainer.add(new AjaxLink("recentlyCreated") {
public void onClick(AjaxRequestTarget target) {
SortParam sort = new SortParam("creationDate", false);
updateTable(columns, sort, null);
target.add(listContainer);
}
});
add(listContainer.setOutputMarkupId(true));
modelChanged();
}
//Truncated for brevity...
public class SearchPanel extends Panel {
public SearchPanel(String id) {
super(id);
final Model m = new Model("");
TextField textField = new TextField("searchString",m, String.class);
textField.setOutputMarkupId(true);
textField.setMarkupId("myuniqueid");
textField.add( new AjaxFormSubmitBehavior("customevent") {
protected void onSubmit(AjaxRequestTarget target) {
if (StringUtils.isEmpty(m.getObject())) {
return;
}
SortParam sort = new SortParam("lastModifedDate", false);
updateTable(columns, sort, m.getObject());
target.add(listContainer);
}
} );
add(textField);
}
}
}
The VideoBrowserDialog renders the video picker and supports all of its features (preview, search, sorting, pagination).
Step 5: Patch External Link Picker
The final piece to implementing the custom picker is a patch to org.onehippo.cms7.channelmanager.widgets.ExtLinkPicker. Out of the box, the standard document picker is hardcoded. We implemented our video picker as a custom Wicket Dialog called VideoBrowserDialog So, we add a new method to ExtLinkPicker that instantiates our custom VideoBrowserDialog instead:
private static IDialogFactory createDialogFactory(final IPluginContext context, final IPluginConfig config, final IModel model, final String providerId) {
return new IDialogFactory() {
public AbstractDialog createDialog() {
return new VideoBrowserDialog(context, config, (IModel)model, providerId, true);
}
};
}
Then, when the configuration dictates that the picker is a video picker, we use this custom factory. In the anonymous instance of ExtEventListener (instantiated in the ExtLinkPicker constructor), we add a condition that uses the new createDialogMethod:
IPluginConfig pickerConfig = parsePickerConfig(pickerConfigObject, current, isRelativePath, rootPath);
if ("cms-pickers/videos".equals(pickerConfig.getString("cluster.name"))) {
IPluginConfigService pluginConfigService = context.getService(IPluginConfigService.class.getName(), IPluginConfigService.class);
if (StringUtils.isEmpty(rootPath)) {
throw new WicketRuntimeException("root path needs to be set on the component");
}
VideoProviderFactory videoProviderFactory = null;
try {
videoProviderFactory = new VideoProviderFactory(pluginConfigService,provider);
} catch (Exception e) {
log.error("Failed to instantiate video provider factory", e);
}
VideoPickedNodeModel videoPickedNodeModel = new VideoPickedNodeModel(target);
final IDialogFactory dialogFactory = createDialogFactory(context, videoProviderFactory.getVideoConfig(), videoPickedNodeModel, provider);
final IDialogService dialogService = context.getService(IDialogService.class.getName(), IDialogService.class);
videoPickedNodeModel.enableEvents();
final DialogAction action = new DialogAction(dialogFactory, dialogService);
action.execute();
}
Step 6: Render the View
Finally, we render the video on the frontend within a JSP by referencing the ${videoId} property:
<script language="JavaScript" type="text/javascript" src="http://admin.brightcove.com/js/BrightcoveExperiences.js"></script>
<object id="myExperience4553177110001" class="BrightcoveExperience">
<param name="bgcolor" value="#FFFFFF" />
<param name="width" value="700" />
<param name="height" value="242" />
<param name="playerID" value="2179486411001" />
<param name="playerKey" value="------------" />
<param name="isVid" value="true" />
<param name="isUI" value="true" />
<param name="dynamicStreaming" value="true" />
<param name="@videoPlayer" value="${videoId}" />
</object>
<script type="text/javascript">
if ( brightcove != null ) {
brightcove.createExperiences();
}
</script>
Conclusion and Further Reading
Video is one of, if not the most, engaging and effective forms of marketing and communication. With businesses both large and small implementing internal and external video strategies, the integration of your CMS with OVMPs has a compelling return on investment.
Hippo's flexible CMS framework and API-centric architecture is a great foundation for dozens of likely integrations, including commerce, CRM, Single-Sign-On, document management, marketing automation and so on. This integration is just one of the many you should consider with Hippo CMS as the core of your content and experience management technology ecosystem.
For further reading, check out the Brightcove API Reference and the Hippo Plugin Reference