Last week, I left you with most of the coded needed to do a custom video conversion inside a work instance. Which means that if you’ve read the first part as well you should know how to do a custom video export workflow. What’s left is to actually display those exported videos. As it requires some code outside of Nuxeo Studio, we need to make Studio aware of the code we’ve written.

Usually, when you want to do this you need to use the registries. That’s what you do when you want to make Studio aware of your custom operations, for instance. So here we can make it aware of the VideoExport facet by adding the following JavaScript in the Document Facets registry.

{
  facets: [    {
      id: "VideoExport",
      label: "Video Export  Facet",
      description: "This facet is added automatically by the export workflow",
      schemas: ["videoExport"],
    }
  ]
}

Having the facet declared in Studio is useful because we can use it to filter a new tab that we can create to display the exported videos. This tab will display a custom widget that shows a Download and Remove button for each exported video. Here’s the code of the widget:

<div xmlns:nxu="http://nuxeo.org/nxweb/util"
     xmlns:ui="http://java.sun.com/jsf/facelets"
     xmlns:f="http://java.sun.com/jsf/core"
     xmlns:h="http://java.sun.com/jsf/html"
     xmlns:c="http://java.sun.com/jstl/core"
     class="content_block">

  <c:if test="#{fieldOrValue.hasFacet('VideoExport')}">

  <c:if test="#{!empty widget.label and widget.handlingLabels}">
    <ui:include src="/widgets/incl/widget_label_template.xhtml">
      <ui:param name="labelStyleClass" value="summaryTitle" />
    </ui:include>
  </c:if>

  <table cellspacing="0">
    <tbody>
      <c:forEach var="exportedVideo" items="#{videoExportActions.getExportedVideos(fieldOrValue)}"
        varStatus="status">
        <tr class="videoConversionRow">
          <td class="fieldColumn">
            #{exportedVideo.name}
          </td>
          <td class="actionsColumn">
           <h:outputLink value="#{videoExportActions.getTranscodedVideoURL(fieldOrValue, exportedVideo)}">
            <h:graphicImage url="/icons/download.png" />
           </h:outputLink>
           <h:commandButton value="remove"
             action="#{videoExportActions.removeExport(fieldOrValue, exportedVideo)}" class="button smallButton" />
          </td>
        </tr>
      </c:forEach>
    </tbody>
  </table>

  </c:if>

</div>

To use this template widget you need to upload it. Every time you need to upload something in Nuxeo Studio everything happens in the Resources tab - here under the Widgets templates category.

Video Export Widget

Most of the time when you add a custom widget it’s because you need some custom logic. So here I’ve written a SEAM bean to cover all the exported videos download URLs and removal.

package org.nuxeo.video;

import static org.nuxeo.video.ExportedVideoConstants.EXPORTED_VIDEOS_PROPERTY;

import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Install;
import org.jboss.seam.annotations.Name;
import org.nuxeo.ecm.core.api.ClientException;
import org.nuxeo.ecm.core.api.ClientRuntimeException;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.model.PropertyException;
import org.nuxeo.ecm.platform.ui.web.tag.fn.DocumentModelFunctions;

import com.google.common.collect.Maps;

@Name("videoExportActions")
@Install(precedence = Install.FRAMEWORK)
public class VideoExportActions implements Serializable {

    private static final long serialVersionUID = 1L;

    @In(create = true, required = false)
    protected transient CoreSession documentManager;

    private Map<String, ExportedTranscodedVideo> exportedVideos;

    public String getTranscodedVideoURL(DocumentModel doc,
            ExportedTranscodedVideo transcodedVideo) {
        String blobPropertyName = transcodedVideo.getBlobPropertyName();
        return DocumentModelFunctions.bigFileUrl(doc, blobPropertyName,
                transcodedVideo.getBlob().getFilename());
    }

    public void removeExport(DocumentModel doc,
            ExportedTranscodedVideo transcodedVideo) throws PropertyException,
            ClientException {
        List<Map<String, Serializable>> videos = (List<Map<String, Serializable>>) doc.getPropertyValue(EXPORTED_VIDEOS_PROPERTY);
        videos.remove(transcodedVideo.getPosition());
        doc.setPropertyValue(EXPORTED_VIDEOS_PROPERTY, (Serializable) videos);
        documentManager.saveDocument(doc);
    }

    public Collection<ExportedTranscodedVideo> getExportedVideos(
            DocumentModel doc) {
        if (exportedVideos == null) {
            initExportedVideos(doc);
        }
        return exportedVideos.values();
    }

    private void initExportedVideos(DocumentModel doc) {
        try {
            if (exportedVideos == null) {
                @SuppressWarnings("unchecked")
                List<Map<String, Serializable>> videos = (List<Map<String, Serializable>>) doc.getPropertyValue(EXPORTED_VIDEOS_PROPERTY);
                exportedVideos = Maps.newHashMap();
                for (int i = 0; i < videos.size(); i++) {
                    ExportedTranscodedVideo transcodedVideo = ExportedTranscodedVideo.fromMapAndPosition(
                            videos.get(i), i);
                    exportedVideos.put(transcodedVideo.getName(),
                            transcodedVideo);
                }
            }
        } catch (ClientException e) {
            throw new ClientRuntimeException(e);
        }
    }
}

Part of the code is wrapped in ExportedTranscodedVideo. It’s basically the same class as TranscodedVideo, but I had to change the property path because I am using a different schema.

/*
 * (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser General Public License
 * (LGPL) version 2.1 which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl-2.1.html
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * Contributors:
 *     Thomas Roger <[email protected]>
 */

package org.nuxeo.video;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.platform.video.Video;
import org.nuxeo.ecm.platform.video.VideoInfo;

public class ExportedTranscodedVideo extends Video {

    private static final String NAME = "name";

    private static final String CONTENT = "content";

    private static final String INFO = "info";

    private final String name;

    private final int position;

    /**
     * Build a {@code TranscodedVideo} from a {@code Map} of attributes and a
     * {@code position}
     */
    public static ExportedTranscodedVideo fromMapAndPosition(
            Map<String, Serializable> map, int position) {
        Blob blob = (Blob) map.get(CONTENT);
        @SuppressWarnings("unchecked")
        Map<String, Serializable> info = (Map<String, Serializable>) map.get(INFO);
        VideoInfo videoInfo = VideoInfo.fromMap(info);
        String name = (String) map.get(NAME);
        return new ExportedTranscodedVideo(blob, videoInfo, name, position);
    }

    /**
     * Build a {@code TranscodedVideo} from a {@code name}, video {@code blob}
     * and related {@code videoInfo}.
     */
    public static ExportedTranscodedVideo fromBlobAndInfo(String name,
            Blob blob, VideoInfo videoInfo) {
        return new ExportedTranscodedVideo(blob, videoInfo, name, -1);
    }

    private ExportedTranscodedVideo(Blob blob, VideoInfo videoInfo,
            String name, int position) {
        super(blob, videoInfo);
        this.name = name;
        this.position = position;
    }

    /**
     * Returns the name of this {@code TranscodedVideo}.
     */
    public String getName() {
        return name;
    }

    /**
     * Returns the video {@code Blob} property name of this
     * {@code TranscodedVideo}.
     */
    public String getBlobPropertyName() {
        if (position == -1) {
            throw new IllegalStateException(
                    "This transcoded video is not yet persisted, cannot generate property name.");
        }
        StringBuilder sb = new StringBuilder();
        sb.append(ExportedVideoConstants.EXPORTED_VIDEOS_PROPERTY);
        sb.append("/");
        sb.append(position);
        sb.append("/content");
        return sb.toString();
    }

    public int getPosition() {
        return position;
    }

    /**
     * Returns a {@code Map} of attributes for this {@code TranscodedVideo}.
     * <p>
     * Used when saving this {@code TranscodedVideo} to a {@code DocumentModel}
     * property.
     */
    public Map<String, Serializable> toMap() {
        Map<String, Serializable> map = new HashMap<String, Serializable>();
        map.put(NAME, name);
        map.put(CONTENT, (Serializable) blob);
        map.put(INFO, (Serializable) videoInfo.toMap());
        return map;
    }

}

Now that everything is ready to use the custom widget, we need to add it to our tab. So get to the Listing & views > Tabs Studio tab and create a new tab. The first thing I do is go to the enablement part and select the VideoExport Facet. This way the tab will only be displayed for videos with export. Then, I add a simple grid with one row and a Custom Widget template where I select the one I uploaded at the beginning. Don’t forget to check the Add surrounding form option if you want the remove button to work.

Layout Widget Editor

And that’s it. Now if you deploy everything and go to a video document with export, you should see the custom tab:

Export Result