Android: Reproducir vídeo desde URL de Google Drive usando VideoView

Standard

google_drive_logo_3963Qué tal. En el post de hoy, retomaré un tema visto antes en este mismo blog pero en una faceta diferente a la del ejemplo anterior. Hablamos antes sobre como reproducir un vídeo en la vista VideoView de Android, pero en aquella ocasión, para fines prácticos, solo se reprodujo un vídeo previamente añadido a los resources de nuestra aplicación.

Hace unas semanas uno de nuestros lectores solicitó ayuda sobre cómo reproducir un vídeo mediante una URL de Google Drive directamente en la vista VideoView, y me parece una buena idea compartir con ustedes la solución que se encontró para ese caso específico. Para esto, se tomó como referencia una respuesta de StackOverflow consultada para la decodificación de la URL en cuestión.

Entonces, sin mayor preámbulo, pasemos a la implementación de nuestro ejemplo. Pueden encontrar en GitHub el CÓDIGO COMPLETO DEL PROYECTO, ya que en el presente post sólo se mostrarán las partes importantes del mismo.

Construcción de la app

1. Creación del layout (vista) y sus resources

Tendremos sólo una vista, bastante básica, en la cual podremos visualizar el video directamente desde Google Drive. El vídeo elegido para nuestro ejemplo se trata de un timelapse realizado por Knate Myers Photography, por si hay algún interesado en conocer más de su trabajo. Mediante el código mostrado más abajo se podrá conseguir algo como esto:

Screenshot_2016-08-22-16-45-46

 

res/layout/content_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="com.silvia_valdez.multimediaapp.MainActivity"
    tools:showIn="@layout/app_bar_main"
    android:id="@+id/main_layout"
    android:paddingLeft="10dp"
    android:paddingRight="10dp">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textStyle="bold"
        android:textAppearance="@style/TextAppearance.AppCompat.Medium"
        android:gravity="center"
        android:id="@+id/main_text_title"
        android:text="@string/main_title" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/main_image_replay"
        android:src="@mipmap/ic_replay_dark"
        android:layout_below="@+id/main_text_title"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="80dp"
        android:visibility="invisible"
        android:contentDescription="@string/main_replay" />

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/main_text_title"
        android:layout_marginTop="10dp"
        android:visibility="visible"
        android:id="@+id/main_frame">

        <VideoView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/main_video_view"
            android:visibility="visible"
            android:background="@android:color/transparent" />
    </FrameLayout>

</RelativeLayout>

Es importante agregar el correspondiente archivo de strings.xml

<resources>
    <string name="app_name">Multimedia App</string>

    <string name="main_title">Video from URL example</string>
    <string name="main_replay">Replay</string>

    <string name="action_downloading_url">Downloading URL…</string>
    <string name="action_start_video">Starting video…</string>

    <string name="error_failed_connecting">Failed to connect with the server</string>
</resources>

Y en nuestro AndroidManifest.xml, los permisos correspondientes:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.silvia_valdez.multimediaapp"
    android:installLocation="preferExternal">

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission android:name="android.permission.INTERNET"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/Theme.AppCompat">
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:theme="@style/AppTheme.NoActionBar"
            android:configChanges="orientation"
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

2. Creación del Activity

Ya que el código se encuentra documentado, únicamente mencionaré que los puntos clave de nuestra aplicación ejemplo son los siguientes:

  1. Inicialización de las vistas (especificando cuáles se encuentran visibles y cuáles no, por ejemplo).
  2. Asignación de los listeners necesarios para actuar en los eventos más importantes.
  3. Decodificación de nuestra URL de Google Drive para la descarga del contenido multimedia. Esto debido a que la URL debe tener cierto tratamiento para ser interpretada de manera correcta.
  4. Reproducción del vídeo en la vista VideoView.
  5. Guardar y restaurar el momento de la reproducción del vídeo en el que nos encontrábamos en caso de un cambio de estado de nuestra app (cuando esta pasa a segundo plano, etc.). Esto mediante onSaveInstanceState y onRestoreInstanceState.

Es muy importante destacar que para que sea posible descargar el vídeo, este DEBE SER PÚBLICO, ya que de otra manera surgirían problemas posiblemente no tan fáciles de detectar y la reproducción del mismo sería imposible. Otro detalle evidente pero que a veces se nos puede pasar, es que debemos tener previamente activado el Wi-Fi en nuestro dispositivo.

A continuación el código:

 

java/MainActivity.java

private static final String TAG = MainActivity.class.getSimpleName();
private static final String POSITION = "POSITION";

// URL estática del vídeo a reproducir.
private static final String GOOGLE_DRIVE_URL
        = "https://drive.google.com/file/d/0B_yxRIPFWs-KLU5KWlk0YkxUU0E/view?usp=sharing";

private int mPosition = 0;

private VideoView mVideoView;
private ImageView mImageReplay;
private FrameLayout mFrame;
private ProgressDialog mDialog;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    initViews();
    addListenersToViews();
    downloadUrl(GOOGLE_DRIVE_URL);
}

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    super.onSaveInstanceState(savedInstanceState);
    // Usamos onSaveInstanceState para guardar la posición de la reproducción.
    savedInstanceState.putInt(POSITION, mPosition);
    mVideoView.pause();
}

@Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    // Se reproduce el vídeo desde la posición previamente guardada.
    mPosition = savedInstanceState.getInt(POSITION);
    mVideoView.seekTo(mPosition);
}

@Override
protected void onPause() {
    super.onPause();
    // Se obtiene la posición actual de la reproducción antes de la pausa.
    mPosition = mVideoView.getCurrentPosition();
}

/**
 * MÉTODOS.
 */
private void initViews() {
    mImageReplay = (ImageView) findViewById(R.id.main_image_replay);
    if (mImageReplay != null) {
        mImageReplay.setVisibility(View.INVISIBLE);
    }

    /**
     *  Este frame es usado para ocultar el VideoView hasta que se encuentra
     * preparado para mostrar el vídeo.
     **/
    mFrame = (FrameLayout) findViewById(R.id.main_frame);
    if (mFrame != null) {
        mFrame.setVisibility(View.INVISIBLE);
    }

    // Creamos controles multimedia para manipular el vídeo.
    MediaController mediaController = new MediaController(this);

    mVideoView = (VideoView) findViewById(R.id.main_video_view);
    if (mVideoView != null) {
        mVideoView.setMediaController(mediaController);
        mediaController.setAnchorView(mVideoView);
    }
}

private void addListenersToViews() {
    mImageReplay.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            /**
             * En caso de que falle la descarga, se visualizará 
             * un botón para reintentar.
             **/
            downloadUrl(GOOGLE_DRIVE_URL);
        }
    });

    mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
        @Override
        public void onPrepared(MediaPlayer mediaPlayer) {
            // Cuando el vídeo está preparado, minimiza el ProgressDialog.
            if (mDialog.isShowing()) mDialog.dismiss();
            if (mFrame != null) {
                mFrame.setVisibility(View.VISIBLE);
            }

            // Reproduce el vídeo en un loop (se repite al finalizar).
            mediaPlayer.setLooping(true);

            if (mPosition == 0) {
                // Si tenemos una posición guardada, el vídeo comienza ahí.
                mVideoView.start();
            } else {
                // Si nuestro activity se reanuda, ponemos pausa.
                mVideoView.pause();
            }
        }
    });

    mVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
        @Override
        public boolean onError(MediaPlayer mp, int what, int extra) {
            mDialog.dismiss();
            mImageReplay.setVisibility(View.VISIBLE);
            return false;
        }
    });
}

private void initVideoView(String downloadedUrl) {
    mDialog.setMessage(getString(R.string.action_start_video));
    Uri uri = Uri.parse(downloadedUrl);

    if (mVideoView != null) {
        // Se inicia la visualización del vídeo. 
        mVideoView.setVideoURI(uri);
        mVideoView.start();
    }
}

public void downloadUrl(final String strUrl) {
    // Se inicializa un ProgressDialog para mostrar el estatus de la app.
    mDialog = ProgressDialog.show(this, "", 
        getString(R.string.action_downloading_url), true);

    // En un NUEVO THREAD, se inicia la descarga del contenido multimedia.
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                URL url = new URL(strUrl);
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                connection.setReadTimeout(10000);
                connection.setConnectTimeout(15000);
                connection.setRequestMethod("GET");
                connection.setDoInput(true);
                connection.connect();
                InputStream inputStream = connection.getInputStream();
                final String downloadedUrl = readInputStream(inputStream);
                Log.d(TAG, String.format("New URL: %s", downloadedUrl));

                // Al terminar la descarga volvemos al MAIN THREAD.
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        initVideoView(downloadedUrl);
                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mDialog.dismiss();
                        Toast.makeText(getApplicationContext(),
                                R.string.error_failed_connecting,
                                Toast.LENGTH_LONG).show();

                        mImageReplay.setVisibility(View.VISIBLE);
                    }
                });
            }
        }
    }).start();
}

public String readInputStream(InputStream stream) throws IOException {
    BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"));
    StringBuilder stringBuilder = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) {
        if (line.contains("fmt_stream_map")) {
            stringBuilder.append(line).append("\n");
            break;
        }
    }
    reader.close();
    String result = decode(stringBuilder.toString());
    String[] url = result.split("\\|");
    return url[1];
}

public String decode(String url) {
    String working = url;
    int index;
    index = working.indexOf("\\u");
    while (index > -1) {
        int length = working.length();
        if (index > (length - 6)) break;
        int numStart = index + 2;
        int numFinish = numStart + 4;
        String substring = working.substring(numStart, numFinish);
        int number = Integer.parseInt(substring, 16);
        String stringStart = working.substring(0, index);
        String stringEnd = working.substring(numFinish);
        working = stringStart + ((char) number) + stringEnd;
        index = working.indexOf("\\u");
    }
    return working;
}

Y bien, por hoy esto ha sido todo, con este breve ejemplo práctico podemos ahora integrar en nuestras aplicaciones vídeos públicos directamente desde Google Drive. Para cualquier duda o mejora en el código presentado, me encuentro a su disposición. ¡Hasta luego!

 

Referencias:

 

Leave a Reply

Your email address will not be published. Required fields are marked *