Home » Android » Android Applications » Android MediaMetadataRetriever example for local files and Remote URL’s

Android MediaMetadataRetriever example for local files and Remote URL’s

MediaMetadataRetriever class provides a unified interface for retrieving frame and meta data (such as Audio/Video duration if its audio/video file OR height and width if its image ) from an input media file.

// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.content.browser;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.MediaMetadataRetriever;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;

import org.chromium.base.Log;
import org.chromium.base.PathUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * Java counterpart of android MediaResourceGetter.
 */
@JNINamespace("content")
class MediaResourceGetter {
    private static final String TAG = "cr_MediaResource";
    private static final MediaMetadata EMPTY_METADATA = new MediaMetadata(0, 0, 0, false);

    private final MediaMetadataRetriever mRetriever = new MediaMetadataRetriever();

    @VisibleForTesting
    static class MediaMetadata {
        private final int mDurationInMilliseconds;
        private final int mWidth;
        private final int mHeight;
        private final boolean mSuccess;

        MediaMetadata(int durationInMilliseconds, int width, int height, boolean success) {
            mDurationInMilliseconds = durationInMilliseconds;
            mWidth = width;
            mHeight = height;
            mSuccess = success;
        }

        // TODO(andrewhayden): according to the spec, if duration is unknown
        // then we must return NaN. If it is unbounded, then positive infinity.
        // http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html
        @CalledByNative("MediaMetadata")
        int getDurationInMilliseconds() {
            return mDurationInMilliseconds;
        }

        @CalledByNative("MediaMetadata")
        int getWidth() {
            return mWidth;
        }

        @CalledByNative("MediaMetadata")
        int getHeight() {
            return mHeight;
        }

        @CalledByNative("MediaMetadata")
        boolean isSuccess() {
            return mSuccess;
        }

        @Override
        public String toString() {
            return "MediaMetadata["
                    + "durationInMilliseconds=" + mDurationInMilliseconds
                    + ", width=" + mWidth
                    + ", height=" + mHeight
                    + ", success=" + mSuccess
                    + "]";
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + mDurationInMilliseconds;
            result = prime * result + mHeight;
            result = prime * result + (mSuccess ? 1231 : 1237);
            result = prime * result + mWidth;
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null) return false;
            if (getClass() != obj.getClass()) return false;
            MediaMetadata other = (MediaMetadata) obj;
            if (mDurationInMilliseconds != other.mDurationInMilliseconds) return false;
            if (mHeight != other.mHeight) return false;
            if (mSuccess != other.mSuccess) return false;
            if (mWidth != other.mWidth) return false;
            return true;
        }
    }

    @CalledByNative
    private static MediaMetadata extractMediaMetadata(final Context context,
            final String url,
            final String cookies,
            final String userAgent) {
        return new MediaResourceGetter().extract(
                context, url, cookies, userAgent);
    }

    @CalledByNative
    private static MediaMetadata extractMediaMetadataFromFd(int fd,
            long offset,
            long length) {
        return new MediaResourceGetter().extract(fd, offset, length);
    }

    @VisibleForTesting
    MediaMetadata extract(int fd, long offset, long length) {
        configure(fd, offset, length);
        return doExtractMetadata();
    }

    @VisibleForTesting
    MediaMetadata extract(final Context context, final String url,
                          final String cookies, final String userAgent) {
        if (!configure(context, url, cookies, userAgent)) {
            Log.e(TAG, "Unable to configure metadata extractor");
            return EMPTY_METADATA;
        }
        return doExtractMetadata();
    }

    private MediaMetadata doExtractMetadata() {
        try {
            String durationString = extractMetadata(
                    MediaMetadataRetriever.METADATA_KEY_DURATION);
            if (durationString == null) {
                Log.w(TAG, "missing duration metadata");
                return EMPTY_METADATA;
            }

            int durationMillis = 0;
            try {
                durationMillis = Integer.parseInt(durationString);
            } catch (NumberFormatException e) {
                Log.w(TAG, "non-numeric duration: %s", durationString);
                return EMPTY_METADATA;
            }

            int width = 0;
            int height = 0;
            boolean hasVideo = "yes".equals(extractMetadata(
                    MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO));
            Log.d(TAG, (hasVideo ? "resource has video" : "resource doesn't have video"));
            if (hasVideo) {
                String widthString = extractMetadata(
                        MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
                if (widthString == null) {
                    Log.w(TAG, "missing video width metadata");
                    return EMPTY_METADATA;
                }
                try {
                    width = Integer.parseInt(widthString);
                } catch (NumberFormatException e) {
                    Log.w(TAG, "non-numeric width: %s", widthString);
                    return EMPTY_METADATA;
                }

                String heightString = extractMetadata(
                        MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
                if (heightString == null) {
                    Log.w(TAG, "missing video height metadata");
                    return EMPTY_METADATA;
                }
                try {
                    height = Integer.parseInt(heightString);
                } catch (NumberFormatException e) {
                    Log.w(TAG, "non-numeric height: %s", heightString);
                    return EMPTY_METADATA;
                }
            }
            MediaMetadata result = new MediaMetadata(durationMillis, width, height, true);
            Log.d(TAG, "extracted valid metadata: %s", result);
            return result;
        } catch (RuntimeException e) {
            Log.e(TAG, "Unable to extract metadata: %s", e);
            return EMPTY_METADATA;
        }
    }

    @VisibleForTesting
    boolean configure(Context context, String url, String cookies, String userAgent) {
        URI uri;
        try {
            uri = URI.create(url);
        } catch (IllegalArgumentException  e) {
            Log.e(TAG, "Cannot parse uri: %s", e);
            return false;
        }
        String scheme = uri.getScheme();
        if (scheme == null || scheme.equals("file")) {
            File file = uriToFile(uri.getPath());
            if (!file.exists()) {
                Log.e(TAG, "File does not exist.");
                return false;
            }
            if (!filePathAcceptable(file, context)) {
                Log.e(TAG, "Refusing to read from unsafe file location.");
                return false;
            }
            try {
                configure(file.getAbsolutePath());
                return true;
            } catch (RuntimeException e) {
                Log.e(TAG, "Error configuring data source: %s", e);
                return false;
            }
        }
        if (scheme.equals("content")) {
            try {
                configure(context, Uri.parse(uri.toString()));
                return true;
            } catch (RuntimeException e) {
                Log.e(TAG, "Error configuring data source: %s", e);
                return false;
            }
        }
        if (uri.getPath() != null && uri.getPath().endsWith(".m3u8")) {
            // MediaMetadataRetriever does not work with HLS correctly.
            return false;
        }
        final String host = uri.getHost();
        if (!isLoopbackAddress(host) && !isNetworkReliable(context)) {
            Log.w(TAG, "non-file URI can't be read due to unsuitable network conditions");
            return false;
        }
        Map<String, String> headersMap = new HashMap<String, String>();
        if (!TextUtils.isEmpty(cookies)) {
            headersMap.put("Cookie", cookies);
        }
        if (!TextUtils.isEmpty(userAgent)) {
            headersMap.put("User-Agent", userAgent);
        }
        try {
            configure(url, headersMap);
            return true;
        } catch (RuntimeException e) {
            Log.e(TAG, "Error configuring data source: %s", e);
            return false;
        }
    }

    /**
     * @return true if the device is on an ethernet or wifi network.
     * If anything goes wrong (e.g., permission denied while trying to access
     * the network state), returns false.
     */
    @VisibleForTesting
    boolean isNetworkReliable(Context context) {
        if (context.checkCallingOrSelfPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
                != PackageManager.PERMISSION_GRANTED) {
            Log.w(TAG, "permission denied to access network state");
            return false;
        }

        Integer networkType = getNetworkType(context);
        if (networkType == null) {
            return false;
        }
        switch (networkType.intValue()) {
            case ConnectivityManager.TYPE_ETHERNET:
            case ConnectivityManager.TYPE_WIFI:
                Log.d(TAG, "ethernet/wifi connection detected");
                return true;
            case ConnectivityManager.TYPE_WIMAX:
            case ConnectivityManager.TYPE_MOBILE:
            default:
                Log.d(TAG, "no ethernet/wifi connection detected");
                return false;
        }
    }

    // This method covers only typcial expressions for the loopback address
    // to resolve the hostname without a DNS loopup.
    private boolean isLoopbackAddress(String host) {
        return host != null &amp;&amp; (host.equalsIgnoreCase("localhost")  // typical hostnames
                || host.equalsIgnoreCase("localhost.localdomain")
                || host.equalsIgnoreCase("localhost6")
                || host.equalsIgnoreCase("localhost6.localdomain6")
                || host.toLowerCase(Locale.US).endsWith(".localhost")
                || host.equals("127.0.0.1")  // typical IP v4 expression
                || host.equals("[::1]"));  // typical IP v6 expression
    }

    /**
     * @param file the file whose path should be checked
     * @return true if and only if the file is in a location that we consider
     * safe to read from, such as /mnt/sdcard.
     */
    @VisibleForTesting
    boolean filePathAcceptable(File file, Context context) {
        final String path;
        try {
            path = file.getCanonicalPath();
        } catch (IOException e) {
            // Canonicalization has failed. Assume malicious, give up.
            Log.w(TAG, "canonicalization of file path failed");
            return false;
        }
        // In order to properly match the roots we must also canonicalize the
        // well-known paths we are matching against. If we don't, then we can
        // get unusual results in testing systems or possibly on rooted devices.
        // Note that canonicalized directory paths always end with '/'.
        List<String> acceptablePaths = canonicalize(getRawAcceptableDirectories(context));
        acceptablePaths.add(getExternalStorageDirectory());
        Log.d(TAG, "canonicalized file path: %s", path);
        for (String acceptablePath : acceptablePaths) {
            if (path.startsWith(acceptablePath)) {
                return true;
            }
        }
        return false;
    }

    // The methods below can be used by unit tests to fake functionality.
    @VisibleForTesting
    File uriToFile(String path) {
        return new File(path);
    }

    @VisibleForTesting
    Integer getNetworkType(Context context) {
        // TODO(qinmin): use ConnectionTypeObserver to listen to the network type change.
        ConnectivityManager mConnectivityManager =
                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        if (mConnectivityManager == null) {
            Log.w(TAG, "no connectivity manager available");
            return null;
        }
        NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
        if (info == null) {
            Log.d(TAG, "no active network");
            return null;
        }
        return info.getType();
    }

    @SuppressLint("SdCardPath")
    private List<String> getRawAcceptableDirectories(Context context) {
        List<String> result = new ArrayList<String>();
        result.add("/mnt/sdcard/");
        result.add("/sdcard/");
        result.add("/data/data/" + context.getPackageName() + "/cache/");
        return result;
    }

    private List<String> canonicalize(List<String> paths) {
        List<String> result = new ArrayList<String>(paths.size());
        try {
            for (String path : paths) {
                result.add(new File(path).getCanonicalPath());
            }
            return result;
        } catch (IOException e) {
            // Canonicalization has failed. Assume malicious, give up.
            Log.w(TAG, "canonicalization of file path failed");
        }
        return result;
    }

    @VisibleForTesting
    String getExternalStorageDirectory() {
        return PathUtils.getExternalStorageDirectory();
    }

    @VisibleForTesting
    void configure(int fd, long offset, long length) {
        ParcelFileDescriptor parcelFd = ParcelFileDescriptor.adoptFd(fd);
        try {
            mRetriever.setDataSource(parcelFd.getFileDescriptor(),
                    offset, length);
        } finally {
            try {
                parcelFd.close();
            } catch (IOException e) {
                Log.e(TAG, "Failed to close file descriptor: %s", e);
            }
        }
    }

    @VisibleForTesting
    void configure(String url, Map<String, String> headers) {
        mRetriever.setDataSource(url, headers);
    }

    @VisibleForTesting
    void configure(String path) {
        mRetriever.setDataSource(path);
    }

    @VisibleForTesting
    void configure(Context context, Uri uri) {
        mRetriever.setDataSource(context, uri);
    }

    @VisibleForTesting
    String extractMetadata(int key) {
        return mRetriever.extractMetadata(key);
    }
}

Reference

Leave a Comment