AEM Cloud Service - Dynamic Media open, download Smart Crops, Video Encodes from Preview Server (Test Context)


Goal

AEM Cloud Version :  2021.3.5087.20210322T071003Z-210225 (March 22, 2021)

Add a button Smart Crop Download in Asset Details action bar to open/download smart crops for images and encodes for videos. Please stress test the logic for large video encode downloads....

Demo | Package Install | Github


Image Smart Crop Download


Video Encode Download



Solution

1) Add a service user eaem-service-user in repo init script ui.config\src\main\content\jcr_root\apps\eaem-cs-smart-crop-open\osgiconfig\config.author\org.apache.sling.jcr.repoinit.RepositoryInitializer-eaem.config

scripts=[
        "
        create service user eaem-service-user with path system/cq:services/experience-aem
        set principal ACL for eaem-service-user
                allow jcr:read on /apps
                allow jcr:all on /conf
        end
        "
]


2) Provide the service user to bundle mapping in ui.config\src\main\content\jcr_root\apps\eaem-cs-smart-crop-open\osgiconfig\config.author\org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-ea.xml

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root
        xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
        xmlns:jcr="http://www.jcp.org/jcr/1.0"
        jcr:primaryType="sling:OsgiConfig"
        user.mapping="[eaem-cs-smart-crop-open.core:eaem-service-user=[eaem-service-user]]"/>


3) Add a proxy servlet apps.experienceaem.assets.core.servlets.DynamicRenditionProxy to download the video encodes

package apps.experienceaem.assets.core.servlets;

import apps.experienceaem.assets.core.services.EAEMDMService;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.fluent.Request;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.osgi.services.HttpClientBuilderFactory;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.mime.MimeTypeService;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

@Component(
        name = "Experience AEM Dynamic Rendition Proxy Servlet",
        immediate = true,
        service = Servlet.class,
        property = { "sling.servlet.methods=GET", "sling.servlet.paths=/bin/eaem/proxy" })
public class DynamicRenditionProxy extends SlingAllMethodsServlet {
    private static final Logger log = LoggerFactory.getLogger(DynamicRenditionProxy.class);

    @Reference
    private transient HttpClientBuilderFactory httpClientBuilderFactory;

    private transient CloseableHttpClient httpClient;

    @Reference
    private transient EAEMDMService dmcService;

    @Reference
    private transient ResourceResolverFactory factory;

    @Reference
    private transient MimeTypeService mimeTypeService;

    protected void activate(final ComponentContext ctx) {
        final HttpClientBuilder builder = httpClientBuilderFactory.newBuilder();

        final RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(30000).setSocketTimeout(30000)
                .build();

        builder.setDefaultRequestConfig(requestConfig);

        httpClient = builder.build();
    }

    @Override
    protected final void doGet(final SlingHttpServletRequest request, final SlingHttpServletResponse response)
            throws ServletException, IOException {
        try {
            final String drUrl = request.getParameter("dr");

            if (StringUtils.isEmpty(drUrl)) {
                response.getWriter().print(getAEMIPAddress());
                return;
            }

            downloadImage(response, drUrl);
        } catch (final Exception e) {
            log.error("Could not get response", e);
            response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }

    private String getAEMIPAddress() throws Exception {
        return Request.Get("https://ifconfig.me/ip").execute().returnContent().asString();
    }

    private void downloadImage(final SlingHttpServletResponse response, final String url) throws Exception {
        String fileName = url.substring(url.lastIndexOf("/") + 1);
        final String finalUrl = url.substring(0, url.lastIndexOf("/")) + "/"
                                + URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString());
        fileName = fileName.replaceAll(":", "-");

        log.info("Encoded URL: {}", finalUrl);

        final HttpGet get = new HttpGet(finalUrl);
        final CloseableHttpResponse s7Response = httpClient.execute(get);

        final String contentType = ContentType.get(s7Response.getEntity()).getMimeType();
        fileName = fileName + "." + mimeTypeService.getExtension(contentType);

        response.setContentType("application/octet-stream");
        response.setHeader("Content-disposition", "attachment; filename=" + fileName);

        final InputStream in = s7Response.getEntity().getContent();

        final OutputStream out = response.getOutputStream();

        IOUtils.copy(in, out);

        out.close();

        in.close();
    }

    private void streamImage(final SlingHttpServletResponse response, final String url) throws Exception {
        response.setContentType("image/jpeg");

        final byte[] image = Request.Get(url).execute().returnContent().asBytes();

        final InputStream in = new ByteArrayInputStream(image);

        final OutputStream out = response.getOutputStream();

        IOUtils.copy(in, out);

        out.close();

        in.close();
    }
}


4) Add a service implementation apps.experienceaem.assets.core.services.impl.EAEMDMServiceImpl for executing the S7 API and get preview server / test context url...

package apps.experienceaem.assets.core.services.impl;

import apps.experienceaem.assets.core.services.EAEMDMService;
import com.day.cq.dam.scene7.api.*;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.xpath.*;
import java.util.HashMap;
import java.util.Map;


@Component(service = EAEMDMService.class)
@Designate(ocd = EAEMDMServiceImpl.DMServiceConfiguration .class)
public class EAEMDMServiceImpl implements EAEMDMService {
    private static final Logger log = LoggerFactory.getLogger(EAEMDMServiceImpl.class);

    private static String APPLICATION_TEST_SERVER_CONTEXT = "application_test_server_context";

    private static final String EAEM_SERVICE_USER = "eaem-service-user";

    private String dmcTestContext;

    @Reference
    private ResourceResolverFactory resourceResolverFactory;

    @Reference
    private Scene7Service scene7Service;

    @Reference
    private S7ConfigResolver s7ConfigResolver;

    @Reference
    private Scene7APIClient scene7APIClient;

    @Activate
    @Modified
    protected void activate(final DMServiceConfiguration  config) {
        dmcTestContext = config.dmc_test_context();

        if (StringUtils.isNotEmpty(dmcTestContext)) {
            dmcTestContext = dmcTestContext.trim();

            if (!dmcTestContext.endsWith("/")) {
                dmcTestContext = dmcTestContext + "/";
            }
        }

        log.debug("DMC(S7) test context set in configuration - " + dmcTestContext);
    }

    @Override
    public String getS7TestContext(final String assetPath) {
        if (StringUtils.isNotEmpty(dmcTestContext)) {
            log.info("DMC(S7) test context - " + dmcTestContext);
            return dmcTestContext;
        }

        String testContext = "";

        try {
            final ResourceResolver s7ConfigResourceResolver = getServiceResourceResolver();

            if (s7ConfigResourceResolver  == null) {
                return testContext;
            }

            S7Config s7Config = s7ConfigResolver.getS7ConfigForAssetPath(s7ConfigResourceResolver, assetPath);

            if (s7Config == null) {
                s7Config = s7ConfigResolver.getDefaultS7Config(s7ConfigResourceResolver);
            }

            final String appSettingsTypeHandle = scene7Service.getApplicationPropertyHandle(s7Config);
            final Document document = scene7APIClient.getPropertySets(appSettingsTypeHandle, s7Config);

            testContext = getPropertyValue(document, APPLICATION_TEST_SERVER_CONTEXT);

            if(StringUtils.isEmpty(testContext)){
                testContext = "https://preview1.assetsadobe.com/";
            }

            if (!testContext.endsWith("/")) {
                testContext = testContext + "/";
            }

            log.info("DMC(S7) test context read using api - " + testContext);

            dmcTestContext = testContext;
        } catch (final XPathExpressionException e) {
            log.error("Error getting S7 test context ", e);
        }

        return testContext;
    }

    public String getS7TestContextUrl(final String assetPath, final String deliveryUrl) {
        String testContextUrl = "";

        if (StringUtils.isEmpty(deliveryUrl)) {
            return testContextUrl;
        }

        String imageServerPath = "";

        imageServerPath = deliveryUrl.substring(deliveryUrl.indexOf("/is/image") + 1);

        testContextUrl = getS7TestContext(assetPath) + imageServerPath;

        testContextUrl = testContextUrl.replace("http://", "https://");

        log.debug("Rendition test context url - " + testContextUrl);

        return testContextUrl;
    }

    private String getPropertyValue(final Document document, final String name) throws XPathExpressionException {
        final XPath xpath = XPathFactory.newInstance().newXPath();
        String value = "";

        final String expression = getLocalName("getPropertySetsReturn") + getLocalName("setArray")
                + getLocalName("items") + getLocalName("propertyArray") + getLocalName("items");

        final XPathExpression xpathExpr = xpath.compile(expression);

        final NodeList nodeList = (NodeList) xpathExpr.evaluate(document, XPathConstants.NODESET);
        Node nameNode, valueNode;

        for (int i = 0; i < nodeList.getLength(); i++) {
            nameNode = nodeList.item(i).getFirstChild();

            if (!nameNode.getTextContent().equals(name)) {
                continue;
            }

            valueNode = nodeList.item(i).getLastChild();

            value = valueNode.getTextContent();

            break;
        }

        return value;
    }

    private String getLocalName(final String name) {
        return "/*[local-name()='" + name + "']";
    }

    public ResourceResolver getServiceResourceResolver() {
        Map<String, Object> subServiceUser = new HashMap<>();
        subServiceUser.put(ResourceResolverFactory.SUBSERVICE, EAEM_SERVICE_USER);
        try {
            return resourceResolverFactory.getServiceResourceResolver(subServiceUser);
        } catch (Exception ex) {
            log.error("Could not login as SubService user {}, exiting SearchService service.", "eaem-service-user", ex);
            return null;
        }
    }

    @ObjectClassDefinition(name = "Experience AEM Dynamic Media Configuration")
    public @interface DMServiceConfiguration {

        @AttributeDefinition(
                name = "DMC (S7) test context",
                description = "Set DMC (S7) test context (and not read it using API)",
                type = AttributeType.STRING)
        String dmc_test_context();
    }
}


5) To get the image smart crops as JSON, add script /apps/eaem-cs-smart-crop-open/extensions/image-smart-crops/image-smart-crops.jsp 

<%@include file="/libs/granite/ui/global.jsp"%>

<%@page session="false"
        import="java.util.Iterator,
                  org.apache.sling.commons.json.JSONObject,
                  com.adobe.granite.ui.components.Config,
                  com.adobe.granite.ui.components.Tag"%>
<%@ page import="com.adobe.granite.ui.components.ds.ValueMapResource" %>
<%@ page import="com.adobe.granite.ui.components.ds.DataSource" %>
<%@ page import="org.apache.sling.commons.json.JSONArray" %>
<%@ page import="apps.experienceaem.assets.core.services.EAEMDMService" %>

<%
    Config cfg = cmp.getConfig();
    ValueMap dynVM = null;

    JSONObject dynRenditions = new JSONObject();
    Resource dynResource = null;

    EAEMDMService dmcService = sling.getService(EAEMDMService.class);
    response.setContentType("application/json");

    String name = "Original";

    JSONObject dynRendition = new JSONObject();

    dynRendition.put("type", "IMAGE");
    dynRendition.put("name", name);

    dynRenditions.put(name, dynRendition);

    DataSource rendsDS = null;

    try{
        rendsDS = cmp.getItemDataSource();
    }catch(Exception e){
        //could be pixel crop, ignore...
    }

    if(rendsDS == null){
        dynRenditions.write(response.getWriter());
        return;
    }

    for (Iterator<Resource> items = rendsDS.iterator(); items.hasNext();) {
        dynRendition = new JSONObject();

        dynResource = items.next();

        dynVM = dynResource.getValueMap();

        name = String.valueOf(dynVM.get("breakpoint-name"));
        String testContextUrl = dmcService.getS7TestContextUrl(dynResource.getPath(), (String)dynVM.get("copyurl"));

        dynRendition.put("type", "IMAGE");
        dynRendition.put("name", name);
        dynRendition.put("s7Url", testContextUrl);
        dynRendition.put("cropdata", getCropData(dynVM));

        dynRenditions.put(name, dynRendition);
    }

    dynRenditions.write(response.getWriter());
%>

<%!
    private static JSONArray getCropData(ValueMap dynVM) throws Exception{
        JSONArray cropArray = new JSONArray();
        JSONObject cropData = new JSONObject();

        cropData.put("name", String.valueOf(dynVM.get("breakpoint-name")));
        cropData.put("id", dynVM.get("id"));
        cropData.put("topN", dynVM.get("topN"));
        cropData.put("bottomN", dynVM.get("bottomN"));
        cropData.put("leftN", dynVM.get("leftN"));
        cropData.put("rightN", dynVM.get("rightN"));

        cropArray.put(cropData);

        return cropArray;
    }
%>


6) Set the datasource for Image Smart Crops /apps/eaem-cs-smart-crop-open/extensions/image-smart-crops/renditions/datasource@sling:resourceType = dam/gui/components/s7dam/smartcrop/datasource

7) To get the video encodes as JSON, add script /apps/eaem-cs-smart-crop-open/extensions/video-encodes/video-encodes.jsp 

<%@include file="/libs/granite/ui/global.jsp"%>

<%@page session="false"
        import="java.util.Iterator,
                org.apache.sling.commons.json.JSONObject,
                com.adobe.granite.ui.components.Config,
                com.adobe.granite.ui.components.Tag"%>
<%@ page import="com.adobe.granite.ui.components.ds.ValueMapResource" %>
<%@ page import="org.apache.sling.api.SlingHttpServletRequest" %>
<%@ page import="com.day.cq.dam.api.Asset" %>
<%@ page import="com.day.cq.dam.api.renditions.DynamicMediaRenditionProvider" %>
<%@ page import="com.day.cq.dam.api.Rendition" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.List" %>
<%@ page import="apps.experienceaem.assets.core.services.EAEMDMService" %>

<%
    response.setContentType("application/json");

    SlingHttpServletRequest eaemSlingRequest = slingRequest;
    String assetPath = eaemSlingRequest.getRequestPathInfo().getSuffix();

    Resource currentResource = eaemSlingRequest.getResourceResolver().getResource(assetPath);
    Asset asset = (currentResource != null ? currentResource.adaptTo(Asset.class) : null);

    EAEMDMService dmcService = sling.getService(EAEMDMService.class);
    String s7Domain = dmcService.getS7TestContext(asset.getPath());

    s7Domain = s7Domain.replace("http://", "https://");

    JSONObject dynRenditions = new JSONObject();

    if( (asset == null) || !(asset.getMimeType().startsWith("video/"))) {
        dynRenditions.write(response.getWriter());
        return;
    }

    DynamicMediaRenditionProvider dmRendProvider = sling.getService(DynamicMediaRenditionProvider.class);

    HashMap<String, Object> rules = new HashMap<>();
    rules.put("remote", true);
    rules.put("video", true);

    JSONObject dynRendition = new JSONObject();
    String image = null;
    String s7EncodeUrl = null;

    List<Rendition> dmRenditions = dmRendProvider.getRenditions(asset, rules);

    for (Rendition dmRendition : dmRenditions) {
        dynRendition = new JSONObject();

        image = dmRendition.getPath();

        image = image.substring(0, image.lastIndexOf("."));

        s7EncodeUrl = getPreviewUrl(s7Domain, dmRendition.getPath());

        dynRendition.put("type", "VIDEO");
        dynRendition.put("name", dmRendition.getName());
        dynRendition.put("image", getRendThumbnail(s7Domain, image));
        dynRendition.put("s7Url", s7EncodeUrl);

        dynRenditions.put(dmRendition.getName(), dynRendition);
    }

    dynRenditions.write(response.getWriter());
%>

<%!
    private static String getScene7Url(String s7Domain, String rendPath){
        return s7Domain + "/s7viewers/html5/VideoViewer.html?asset=" + rendPath;
    }

    private static String getPreviewUrl(String s7Domain, String rendPath){
        if(rendPath.contains(".")){
            rendPath = rendPath.substring(0, rendPath.lastIndexOf("."));
        }

        return s7Domain + "is/content/" + rendPath;
    }

    private static String getRendThumbnail(String s7Domain, String rendPath){
        return s7Domain + "is/image/" + rendPath + "?fit=constrain,1&wid=200&hei=200";
    }
%>


8) Set the datasource for video encodes /apps/eaem-cs-smart-crop-open/extensions/video-encodes/renditions/datasource@sling:resourceType = dam/gui/components/s7dam/smartcrop/datasource

9) Add the action bar button Smart Crop Download configuration in /apps/eaem-cs-smart-crop-open/clientlibs/show-smart-crops-url/content/smart-crop-url-but

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/collection/action"
    icon="link"
    target=".cq-damadmin-admin-childpages"
    text="Smart Crop Download"
    variant="actionBar"/>


10) Add a client library /apps/eaem-cs-smart-crop-open/clientlibs/show-smart-crops-url/clientlib with categories=dam.gui.actions.coral and the following logic in /apps/eaem-cs-smart-crop-open/clientlibs/show-smart-crops-url/clientlib/get-smart-crop-link.js to add the button to action bar, read crops and show them in a modal window...

(function ($, $document) {
    "use strict";

    var ASSET_DETAILS_PAGE = "/assetdetails.html",
        initialized = false,
        RENDITION_ACTIVE = ".rendition-active",
        IMAGE_SMART_CROPS_URL = "/apps/eaem-cs-smart-crop-open/extensions/image-smart-crops/renditions.html",
        VIDEO_ENCODES_URL = "/apps/eaem-cs-smart-crop-open/extensions/video-encodes/renditions.html",
        BESIDE_ACTIVATOR = "cq-damadmin-admin-actions-download-activator",
        PROXY_SERLVET = "/bin/eaem/proxy?dr=",
        SMART_CROP_BUTTON_URL = "/apps/eaem-cs-smart-crop-open/clientlibs/show-smart-crops-url/content/smart-crop-url-but.html";

    if (!isAssetDetailsPage()) {
        return;
    }

    $document.on("foundation-contentloaded", addActionBarButtons);

    function addActionBarButtons(){
        if (initialized) {
            return;
        }

        initialized = true;

        $.ajax(SMART_CROP_BUTTON_URL).done(addSmartCropUrlButton);
    }

    function addSmartCropUrlButton(html) {
        var $eActivator = $("." + BESIDE_ACTIVATOR);

        if ($eActivator.length == 0) {
            return;
        }

        var $smartCropBUt = $(html).insertAfter($eActivator);

        $smartCropBUt.find("coral-button-label").css("padding-left", "7px");
        $smartCropBUt.click(showSmartCropUrl);
    }

    function showSmartCropUrl() {
        var $activeRendition = $(RENDITION_ACTIVE);

        if (_.isEmpty($activeRendition)) {
            showAlert("Rendition not selected...", "Error");
            return;
        }

        var title = $activeRendition.attr("title"),
            assetUrl = window.location.pathname.substring(ASSET_DETAILS_PAGE.length),
            assetMimeType = $(RENDITION_ACTIVE).attr("data-type"),
            url = IMAGE_SMART_CROPS_URL;

        if (assetMimeType && assetMimeType.toLowerCase().startsWith("video")) {
            url = VIDEO_ENCODES_URL;
        } else {
            title = $activeRendition.find(".name").last().html();
        }

        return $.ajax({url: url + assetUrl}).done(function (data) {
            var drUrl = data[title];

            if (!drUrl) {
                showAlert("Dynamic rendition url not available", "Error");
                return;
            }

            var fui = $(window).adaptTo("foundation-ui"),
                options = [{
                    id: "DOWNLOAD",
                    text: "Download"
                },
                {
                    id: "OPEN_TAB",
                    text: "Open"
                },
                {
                    id: "ok",
                    text: "Ok",
                    primary: true
                }];

            fui.prompt("Rendition Url", drUrl["s7Url"], "default", options, function (actionId) {
                if (actionId === "OPEN_TAB") {
                    window.open(drUrl["s7Url"], '_blank');
                }else if (actionId === "DOWNLOAD") {
                    var downloadUrl = PROXY_SERLVET + drUrl["s7Url"];
                    window.open(downloadUrl, '_blank');
                }
            });
        });
    }

    function showAlert(message, title, type, callback) {
        var fui = $(window).adaptTo("foundation-ui"),
            options = [{
                id: "ok",
                text: "Ok",
                primary: true
            }];

        message = message || "Unknown Error";
        title = title || "Error";
        type = type || "warning";

        fui.prompt(title, message, type, options, callback);
    }

    function isAssetDetailsPage() {
        return (window.location.pathname.indexOf(ASSET_DETAILS_PAGE) >= 0);
    }
}(jQuery, jQuery(document)));


No comments:

Post a Comment