Встроить нативный просмотр видео в приложение gluon

Я должен написать приложение для iOS и Android, которое иногда показывает настроенный видеоплеер на части экрана. Я должен иметь возможность управлять им (искать, воспроизводить, приостанавливать, устанавливать скорость, выбирать видео...). Я знаю, что такие носители еще не поддерживаются в Gluon.

Но можно ли написать подобное в XCode и Android Studio и как-то встроить в приложение Gluon?


person Thorvaldur    schedule 21.11.2016    source источник
comment
Я не уверен в этом. Насколько я знаю, вы можете писать JavaFX-фрагменты на Android, поэтому можно смешивать собственный фрагмент просмотра с фрагментом JavaFX. Но с точки зрения производительности... не знаю. И iOS... Без понятия. Возможно, вам следует спросить, что API MediaPlayer JavaFX может делать на этих платформах.   -  person dzim    schedule 21.11.2016
comment
Спасибо дзим. Причина, по которой я спросил, заключается в том, что я считаю, что JavaFX media API ничего не может сделать на Android или iOS.   -  person Thorvaldur    schedule 23.11.2016
comment
Это кажется правильным. В соответствии с этим вопросом здесь ( stackoverflow.com/questions/38419634/), вы можете добавить хотя бы аудиовозможности, предоставив собственный интерфейс для собственного проигрывателя. Но видео. Я честно не уверен. Возможно, вам следует соответствующим образом обновить свой вопрос (видеочасть отсутствует в заголовке), и, возможно, ребята из Gluon попытаются помочь вам, сказав, что возможно, а что нет. Но я все еще боюсь, что встроить видеоплеер будет практически невозможно...   -  person dzim    schedule 23.11.2016
comment
Мне также нужно было показывать видео, и мы остановились на Intent к установленному видеоплееру. Вы можете найти инструкции в моем ответе на эту тему: stackoverflow.com/questions/40671626/ #edit: пожалуйста, проголосуйте за ответ, когда сообщение StackOverflow, которое я предоставил, действительно помогло ...   -  person dzim    schedule 02.12.2016
comment
И еще одна идея: вы можете сделать это, как Facebook, с чатами: stackoverflow.com/questions/15975988/ - где я предполагаю, что заголовок чата может быть чем-то вроде обычного, но native< /b> вид. Я предполагаю, что есть расширения SurfaceView для воспроизведения видео...   -  person dzim    schedule 02.12.2016
comment
Спасибо еще раз. Проект был отложен на несколько недель, но я обязательно рассмотрю ваше решение, когда меня переназначат на него.   -  person Thorvaldur    schedule 05.12.2016
comment
@dzim, мы делаем возможным то, что казалось невозможным... Посмотрите на мой ответ ниже. И его можно сочетать с вашим VolumeService :)   -  person José Pereda    schedule 17.12.2016


Ответы (2)


Следуя шаблонам проектирования в библиотеке Gluon Charm Down, это может быть базовой реализацией Android. VideoService.

Он основан на этом руководстве и адаптирован для использования в текущем SurfaceView, который использует JavaFX. Он создаст TextureView, который будет размещен в центре экрана поверх текущего вида, занимая 95% его ширины.

С плагином Gluon для вашей IDE создайте проект с одним представлением.

  1. Поместите эти два класса в Source Packages, package com.gluonhq.charm.down.plugins:

Интерфейс VideoService

package com.gluonhq.charm.down.plugins;

public interface VideoService {
    void play(String videoName);
    void stop();
    void pause();
    void resume();
}

Класс VideoServiceFactory

package com.gluonhq.charm.down.plugins;

import com.gluonhq.charm.down.DefaultServiceFactory;

public class VideoServiceFactory extends DefaultServiceFactory<VideoService> {

    public VideoServiceFactory() {
        super(VideoService.class);
    }

}
  1. Пакет Android: поместите этот класс в пакеты Android/Java, пакет com.gluonhq.charm.down.plugins.android:

Класс AndroidVideoService

package com.gluonhq.charm.down.plugins.android;

import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.graphics.SurfaceTexture;
import android.media.MediaMetadataRetriever;
import android.media.MediaPlayer;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Surface;
import android.view.TextureView;
import android.view.WindowManager;
import android.widget.RelativeLayout;
import com.gluonhq.charm.down.plugins.VideoService;
import java.io.IOException;
import javafxports.android.FXActivity;

public class AndroidVideoService implements VideoService, TextureView.SurfaceTextureListener {
    private static final String TAG = AndroidVideoService.class.getName();
    private MediaPlayer mMediaPlayer;
    private String videoName;

    private final RelativeLayout relativeLayout;
    private final TextureView textureView;
    private final DisplayMetrics displayMetrics;

    public AndroidVideoService() {
        displayMetrics = new DisplayMetrics();
        WindowManager windowManager = (WindowManager) FXActivity.getInstance().getSystemService(Context.WINDOW_SERVICE);
        windowManager.getDefaultDisplay().getMetrics(displayMetrics);

        relativeLayout = new RelativeLayout(FXActivity.getInstance());

        textureView = new TextureView(FXActivity.getInstance());
        textureView.setSurfaceTextureListener(this);
        relativeLayout.addView(textureView);
    }

    @Override
    public void play(String videoName) {
        this.videoName = videoName;
        stop();
        FXActivity.getInstance().runOnUiThread(() -> {
            FXActivity.getViewGroup().addView(relativeLayout);
        });
    }

    @Override
    public void stop() {
        if (mMediaPlayer != null) {
            mMediaPlayer.stop();
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
        if (relativeLayout != null) {
            FXActivity.getInstance().runOnUiThread(() -> {
                FXActivity.getViewGroup().removeView(relativeLayout);
            });
        }
    }

    @Override
    public void pause() {
        if (mMediaPlayer != null) {
            mMediaPlayer.pause();
        }
    }

    @Override
    public void resume() {
        if (mMediaPlayer != null) { 
            mMediaPlayer.start();
        }
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture st, int i, int i1) {
        Surface surface = new Surface(st);
        try {
            AssetFileDescriptor afd = FXActivity.getInstance().getAssets().openFd(videoName);
            calculateVideoSize(afd);
            mMediaPlayer = new MediaPlayer();
            mMediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
            mMediaPlayer.setSurface(surface);
            mMediaPlayer.setLooping(true);
            mMediaPlayer.prepareAsync();
            mMediaPlayer.setOnPreparedListener(mediaPlayer -> mediaPlayer.start());

        } catch (IllegalArgumentException | SecurityException | IllegalStateException | IOException e) {
            Log.d(TAG, e.getMessage());
        }
    }

    @Override public void onSurfaceTextureSizeChanged(SurfaceTexture st, int i, int i1) { }
    @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture st) { return true; }
    @Override public void onSurfaceTextureUpdated(SurfaceTexture st) { }

    private void calculateVideoSize(AssetFileDescriptor afd) {
        try {
            MediaMetadataRetriever metaRetriever = new MediaMetadataRetriever();
            metaRetriever.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
            String height = metaRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
            String width = metaRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
            double factor = Double.parseDouble(width) > 0 ? Double.parseDouble(height) / Double.parseDouble(width) : 1d;
            // 95% screen width
            RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams((int) (0.95 * displayMetrics.widthPixels), 
                    (int) (0.95 * displayMetrics.widthPixels * factor));
            lp.addRule(RelativeLayout.CENTER_IN_PARENT);
            textureView.setLayoutParams(lp);
        } catch (NumberFormatException e) {
            Log.d(TAG, e.getMessage());
        }
    }
}
  1. Образец

Поместите видеофайл в папку android/assets, например big_buck_bunny.mp4, который можно загрузить с сайта здесь.

Базовый вид

public class BasicView extends View {

    private boolean paused;

    public BasicView(String name) {
        super(name);
    }

    @Override
    protected void updateAppBar(AppBar appBar) {
        appBar.setNavIcon(MaterialDesignIcon.MENU.button());
        appBar.setTitleText("Video View");
        // big_buck_bunny.mp4 video in src/android/assets:
        Services.get(VideoService.class).ifPresent(video -> {
            appBar.getActionItems().add(MaterialDesignIcon.PLAY_ARROW.button(e -> video.play("big_buck_bunny.mp4")));
            appBar.getActionItems().add(MaterialDesignIcon.PAUSE.button(e -> {
                if (!paused) {
                    video.pause();
                    paused = true;
                } else {
                    video.resume();
                    paused = false;
                }
            }));
            appBar.getActionItems().add(MaterialDesignIcon.STOP.button(e -> video.stop()));
        });
    }

}

Разверните на своем устройстве Android и протестируйте:

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

person José Pereda    schedule 17.12.2016
comment
Хороший!! Попробую это в понедельник. Первое, что я делаю с утра! ???? - person dzim; 17.12.2016
comment
Это выглядит очень многообещающе. Спасибо. Я обязательно попробую это, когда мне снова назначат это. - person Thorvaldur; 19.12.2016
comment
Как вы думаете, можно ли реализовать и iOS? Я думаю, что мне понадобится что-то похожее на Android-методы onSurfaceTexture, лежащие в основе JNI, чтобы включить мой плеер. - person Thorvaldur; 20.12.2016
comment
Конечно, это возможно. Я постараюсь добавить его к ответу, когда у меня будет время. - person José Pereda; 20.12.2016
comment
@josé-pereda: Вы нашли время, чтобы сделать внедрение и в IOS? Было бы интересно узнать... - person dzim; 30.01.2017
comment
@dzim да, у меня работает реализация для iOS. Однако мне пришлось использовать фреймворк AVKit, который в настоящее время не включен в плагин fxmobile. - person José Pereda; 05.02.2017

Собственный видеоплеер (или в данном случае метод «предпросмотра» видео) использовался в следующем примере:

https://gist.github.com/bgmf/d87a2bac0a5623f359637a3da334f980

Помимо некоторых предварительных условий, код выглядит так:

package my.application;

import ch.cnlab.disentis.util.Constants;
import org.robovm.apple.foundation.*;
import org.robovm.apple.uikit.UIApplication;
import org.robovm.apple.uikit.UIDocumentInteractionController;
import org.robovm.apple.uikit.UIDocumentInteractionControllerDelegateAdapter;
import org.robovm.apple.uikit.UIViewController;

import java.io.File;
import java.util.logging.Logger;

public class NativeVideoServiceIOS extends PathHelperIOS implements NativeVideoService {
    private static final Logger LOG = Logger.getLogger(NativeVideoServiceIOS.class.getName());

    public NativeVideoServiceIOS() {
        LOG.warning("Initialized Native Video Service with path: " + this.pathBase);
    }

    @Override
    public void triggerPlatformApp(String filename) {
        String fullfile = pathBase.getAbsolutePath() + filename;
        NSURL url = new NSURL(NSURLScheme.File, "", fullfile);
        UIDocumentInteractionController popup = new UIDocumentInteractionController(url);
        popup.setDelegate(new UIDocumentInteractionControllerDelegateAdapter() {
            @Override
            public UIViewController getViewControllerForPreview(UIDocumentInteractionController controller) {
                return UIApplication.getSharedApplication()
                        .getWindows().first().getRootViewController();
            }
        });
        popup.presentPreview(true);
    }

}
person dzim    schedule 10.02.2017