пятница, 14 сентября 2012 г.

Xuggler, iPhone / iPad video rotation

Имеется веб-приложение использующее Xuggler для конвертации видео в flv и видео-файл записанный iPhone в портетном режиме. Видео конвертируется успешно, но в результате повернуто на 90 градусов. В таких видео с мобильных устройств фиксируется информация об ориентации записи (угол поворота камеры). QuickTime при воспроизведении определяет и поворачивает видео как нужно. Другие опробованные плееры (VLC, Kaffeine, MPlayer) автоматически видео не поворачивают. Попробовал вывести в эхо печать все метатеги и проперти имеющегося видеофайла. Потом погуглил, поискал.

Нашел что такая инфа относится к EXIF data и используется она не только в изображениях, но и в видео. В видеофайлах EXIF можно прочитать с помощью замечательной утилиты MediaInfo. Она то и показала, что параметр rotation (угол поворота в градусах) хранится в медиатегах видеопотока, а я то искал в контейнере файла :).

Осталось только повернуть кадры на нужный угол rotation (в данном примере я делаю еще и ресайз под нужный размер с учетом пропорций исходного видео). Надеюсь кому-нить еще пригодится:

public class MultimediaContentConverterVideo{
    public void convertOriginal(String urlIn, String urlOut, boolean debug) throws IOException {

        String workingPath = FilenameUtils.getFullPath(urlIn);
        String filenamePrefix = FilenameUtils.getBaseName(urlIn);

        // create a media reader
        IMediaReader reader = ToolFactory.makeReader(urlIn);

        // stipulate that we want BufferedImages created in BGR 24bit color space
        reader.setBufferedImageTypeToGenerate(BufferedImage.TYPE_3BYTE_BGR);

        // create a writer which receives the decoded media from
        // reader, encodes it and writes it out to the specified file
        IMediaWriter writer = ToolFactory.makeWriter(urlOut, reader);

        // add a debug listener to the writer to see media writer events
        if (debug) {
            writer.addListener(ToolFactory.makeDebugListener());
        }

        // read and decode packets from the source file and
        // then encode and write out data to the output file
        VideoRotator rotator = new VideoRotator();
        reader.addListener(rotator);
        rotator.addListener(writer);

        while (reader.readPacket() == null);

    }

    private class VideoRotator extends MediaToolAdapter {

        private int rotate = 0;

        @Override
        public void onVideoPicture(IVideoPictureEvent event) {

            if (rotate != 0) {
                BufferedImage img = event.getImage();
                // rotate
                double theta = Math.PI / 180 * rotate;
                int widthRotated = (img.getWidth() >= img.getHeight()) ? img.getWidth() : img.getHeight();
                int heightRotated = (img.getWidth() >= img.getHeight()) ? img.getWidth() : img.getHeight();
                BufferedImage imgRotated = new BufferedImage(widthRotated, heightRotated, img.getType());
                Graphics2D gRotate = imgRotated.createGraphics();
                AffineTransform transform = new AffineTransform();

                transform.rotate(theta, widthRotated / 2, heightRotated / 2);
                transform.translate((widthRotated - img.getWidth()) / 2, (heightRotated - img.getHeight()) / 2);
                gRotate.drawImage(img, transform, null);
                gRotate.dispose();
                // resize
                int widthResized = widthRotated;
                int heightResized = heightRotated;
                if (heightResized > img.getHeight()) {
                    widthResized = Math.round((1.0f * img.getHeight() / heightResized) * widthResized);
                    heightResized = img.getHeight();
                } else if (widthResized > img.getWidth()) {
                    heightResized = Math.round((1.0f * img.getWidth() / widthResized) * heightResized);
                    widthResized = img.getWidth();
                }
                Graphics2D g = img.createGraphics();
                g.setColor(Color.BLACK);
                g.fillRect(0, 0, img.getWidth(), img.getHeight());
                g.drawImage(imgRotated, (img.getWidth() - widthResized) / 2, (img.getHeight() - heightResized) / 2, widthResized, heightResized, null);
                g.dispose();
            }
            super.onVideoPicture(event);
        }

        @Override
        public void onAddStream(IAddStreamEvent event) {
            int streamIndex = event.getStreamIndex();
            IStream stream = event.getSource().getContainer().getStream(streamIndex);
            IStreamCoder streamCoder = event.getSource().getContainer().getStream(streamIndex).getStreamCoder();
            if (streamCoder.getCodecType() == ICodec.Type.CODEC_TYPE_AUDIO) {
                streamCoder.setSampleRate(44100);
            } else if (streamCoder.getCodecType() == ICodec.Type.CODEC_TYPE_VIDEO) {
                String metaRotate = stream.getMetaData().getValue(META_KEY_ROTATE);
                if (metaRotate != null && metaRotate.matches("\\d+")) {
                    rotate = Integer.valueOf(metaRotate);
                }
            }
            super.onAddStream(event);
        }
    }
}


PS. Скорее всего, видео с других мобильных устройств потребует аналогичной обработки для правильного определения угла поворота видео.

четверг, 30 августа 2012 г.

Решение проблемы с кодировкой в Spring компонентах c использованием @RequestMapping

Добавляем Spring Intercepter для явного указания кодировки в запросе HttpServletRequest:
package ru.myapp.service;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

public class ServiceCharacterEncodingInterceptor extends HandlerInterceptorAdapter {

    private String characterEncoding = "UTF-8";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        request.setCharacterEncoding(characterEncoding);
        return super.preHandle(request, response, handler);
    }

    public void setCharacterEncoding(String characterEncoding) {
        this.characterEncoding = characterEncoding;
    }
}
Прописываем в конфигурации Spring:
    <mvc:interceptors>
        <bean class="ru.myapp.service.ServiceCharacterEncodingInterceptor"/>
    </mvc:interceptors>

Взято отсюда

вторник, 24 июля 2012 г.

Java vs OpenVZ + CentOS kermel update 2.6.32-042

У нас имеется небольшой веб-сервис бегающий на Apache Tomcat 6.0.32, размещенный на VPS. На этом VPS также размещены клиентские сайты под управлением NetCAT CMS - собственно сервис работает для передачи данных из БД этой CMS в мобильное приложение

В прошлую среду (в середину моего отпуска!) VPS ушел в отказ. Точнее сказать начал очень сильно тормозить. Наш админ отправил VPS в restart. После перезапуска работа клиентских сайтов нормализовалась, а вот Tomcat перестал запускаться.

Симптомы:
1. Запуск - в логах может начать что-то выдаваться, а может и ничего не выдаваться
2. Процесс java висит нагрузки на CPU не дает (периодиески по 0.3% скачет)
3. Удалили все приложения запустили чистый tomcat - запустился, работает и отвечает по http.
4. Делаем деплой нашего небольшого приложения. В логах выдается
Jul 18, 2012 4:17:55 PM org.apache.catalina.startup.HostConfig deployDescriptor                                                    
INFO: Deploying configuration descriptor WebApplication.xml
И всё
5. Чистый Tomcat не всегда запускается, иногда крешится:
#                                            
# A fatal error has been detected by the Java Runtime Environment:
#                                            
#  Internal Error (synchronizer.cpp:1429), pid=1083, tid=3040942992
#  guarantee(mid->header()->is_neutral()) failed: invariant              
#
# JRE version: 7.0_05-b06
# Java VM: Java HotSpot(TM) Client VM (23.1-b03 mixed mode, sharing linux-x86 )
# Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#                                        
# An error report file with more information is saved as:
# /srv/java/tomcat6/conf/hs_err_pid1083.log
#
# If you would like to submit a bug report, please visit:
#   http://bugreport.sun.com/bugreport/crash.jsp
#
6. Иногда в Tomcat выдается:
Exception in thread "Reference Handler" java.lang.IllegalMonitorStateException
        at java.lang.Object.notifyAll(Native Method)
        at java.lang.ref.ReferenceQueue.enqueue(ReferenceQueue.java:51)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:129) 


Мои коллеги в моё отсутствие пробовали переустановить JRE, поставить свежую 1.7, ставили новый tomcat. Картина та же. Обращались в техподдержку - явно были произведены какие-то изменения в конфигурации VPS - безрезультатно.

В понедельник перенесли сервис на наш сервер и на VPS клиента включили HTTP-прокси на наш сервак.

Итог

Первое что нашел http://serverfault.com/questions/389152/java-hangin-and-crashing-on-centos-6
Оттуда сюда http://forum.proxmox.com/threads/6998-Best-strategy-to-handle-strange-JVM-errors-inside-VPS

Проверили систему установленную на VPS - Хостинг-провайдер провел обновление ядра - сборка свежая от 10 мая (2.6.32-042stab055.10 #1 SMP Thu May 10 15:38:32 MSD 2012 i686 i686 i386 GNU/Linux)!

По какой-то причине с последней версией ядра в OpenVZ Container-е с 1 CPU Java не работает.

Из предложенных решений в обсуждении по последней ссылке:
1. Увеличить кол-во CPU как минимум до 2-х
2. Откатить обновление.

Мы всю информацию передали хостинг-провайдеру. Благо дело, техподдержка отреагировала очень адекватно - пошла навстречу и увеличила число CPU до 2-х. Всё заработало.

четверг, 28 июня 2012 г.

Liferay вложенные портлеты

У меня есть Vaadin портлет с интерфейсом управления контентом (кнопки вызывающие различные окна редактирования и т.д.) и мне необходимо его подключить к front-end портлету отображения контента.

Как вложить портлет в портлет? В стандартном лайфреевском портлете "Nested portlets"  используется RuntimePortletUtil.processTemplate. В этом же сервисе есть методы вызова портлета processPortlet. На сайте liferay.com статьи описывающий способ программного вызова этих методов - нет.

Метод тыка результата не приносил.

Просмотрел исходники имплементации RuntimePortletImpl - используется PortalUtil.renderPortlet. По запросу гугл выдал исчерпывающий рабочий пример:
http://www.devatwork.nl/2011/07/liferay-embedding-portlets-in-your-portlet/

Единственное не понял зачем при рендере любого портлета выполняется получение PortletBag соответствующее портлету JOURNAL. После экспериментов выяснил, что  для PortalUtil.renderPortlet первым параметром надо передать портальный ServletContext, иначе будет выдаваться "File &quot;/html/portal/render_portlet.jsp&quot; not found"

Но насколько я понимаю servletContext портала из любого портлета можно получить из PortalUtil.getOriginalServletContext(...).getServletContext(). Мой код практически не отличается от приведенного Bert Willems


    public static String renderPortlet(final PortletRequest request, final PortletResponse response, final String portletId, final String queryString) {
        String result = "Error occured while running portlet";
        try {
            // Get servlet request / response
            HttpServletRequest servletRequest = PortalUtil.getHttpServletRequest(request);
            HttpServletResponse servletResponse = PortalUtil.getHttpServletResponse(response);
            HttpServletRequest portalServletRequest = PortalUtil.getOriginalServletRequest(servletRequest);

            // Get theme display
            final ThemeDisplay themeDisplay = (ThemeDisplay) servletRequest.getAttribute(WebKeys.THEME_DISPLAY);
            // Backup current state
            PortletDisplay portletDisplay = themeDisplay.getPortletDisplay();
            PortletDisplay portletDisplayClone = new PortletDisplay();
            portletDisplay.copyTo(portletDisplayClone);
            final Map requestAttributeBackup = new HashMap();
            for (final String key : Collections.list((Enumeration) servletRequest.getAttributeNames())) {
                requestAttributeBackup.put(key, servletRequest.getAttribute(key));
            }
            // Render the portlet as a runtime portlet
            try {
                com.liferay.portal.model.Portlet portlet = PortletLocalServiceUtil.getPortletById(PortalUtil.getCompanyId(request), portletId);
                servletRequest.setAttribute(WebKeys.RENDER_PORTLET_RESOURCE, Boolean.TRUE);
                result = PortalUtil.renderPortlet(portalServletRequest.getServletContext(), servletRequest, servletResponse, portlet, queryString, false);
            } finally {
                // Restore the state
                portletDisplay.copyFrom(portletDisplayClone);
                portletDisplayClone.recycle();
                for (final String key : Collections.list((Enumeration) servletRequest.getAttributeNames())) {
                    if (!requestAttributeBackup.containsKey(key)) {
                        servletRequest.removeAttribute(key);
                    }
                }
                for (final Map.Entry entry : requestAttributeBackup.entrySet()) {
                    servletRequest.setAttribute(entry.getKey(), entry.getValue());
                }
            }
        } catch (Exception ex) {
            log.error(ex.getMessage(), ex);
        }
        return result;
    }

четверг, 31 мая 2012 г.

С момента выхода Liferay CE 6.1 мы решили перейти со старенького монстра 5.2.3 на него.
Первое с чем столкнулся это StackOverflowError. Проявляется после установки приложений (портлетных/тем/hook) и обращения к ним. Страницы с подключенным плагином (портлетом или темой) не открываются, панель управления работает в штатном режиме.

При установке на продакшн сервер эта проблема тоже возникала, однако очистка папок temp и work с последующей перезагрузкой сервера и переустановкой приложения решало проблему.

Однако разработку на 6.1 вести было невозможно, поэтому работал я на Liferay Portal CE 6.0.5

Но сегодня пришлось заняться этой проблемой вплотную: нашему верстальщику требовалось подготовить тему оформления под Liferay 6.1. Вчера он инициировал проект с темой оформления и уперся в StackOverflowException - работать нет возможности

Никакие танцы с бубнами вокруг установленного на его машине Liferay 6.1 не помогали.
Погуглили, нашли обсуждения на форуме http://www.liferay.com/community/forums/-/message_boards/message/12134612 - народ решил свою проблему путем обновления Liferay IDE. Однако мы ею не пользуемся, и явно проблема не в среде а в самом портале. В этом обсуждении упомянули liferay-web.xml и что проблема заключается в том, что туда попадает InvokerFilter. Проверили так и есть.

Поискал в Liferay JIRA и нашел http://issues.liferay.com/browse/LPS-23426. Оказывается использование liferay-web.xml можно вообще выключить в liferay-plugin-package.properties директивой liferay-web-xml-enabled=false

Всё заработало.