Browse Source

Merge pull request #2524 from nextcloud/videoStreaming

Media streaming with NC14 endpoint
Tobias Kaminsky 6 years ago
parent
commit
cdd3a5656a

+ 16 - 0
src/main/java/com/owncloud/android/files/FileMenuFilter.java

@@ -27,6 +27,7 @@ import android.view.Menu;
 import android.view.MenuItem;
 
 import com.owncloud.android.R;
+import com.owncloud.android.authentication.AccountUtils;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
@@ -183,6 +184,7 @@ public class FileMenuFilter {
         filterEncrypt(toShow, toHide, endToEndEncryptionEnabled);
         filterUnsetEncrypted(toShow, toHide, endToEndEncryptionEnabled);
         filterSetPictureAs(toShow, toHide);
+        filterStream(toShow, toHide);
     }
 
     private void filterShareFile(List<Integer> toShow, List<Integer> toHide, OCCapability capability) {
@@ -346,6 +348,15 @@ public class FileMenuFilter {
         }
     }
 
+    private void filterStream(List<Integer> toShow, List<Integer> toHide) {
+        if (mFiles.isEmpty() || !isSingleFile() || !isSingleMedia() ||
+                !AccountUtils.getServerVersion(mAccount).isMediaStreamingSupported()) {
+            toHide.add(R.id.action_stream_media);
+        } else {
+            toShow.add(R.id.action_stream_media);
+        }
+    }
+
     private boolean anyFileSynchronizing() {
         boolean synchronizing = false;
         if (mComponentsGetter != null && !mFiles.isEmpty() && mAccount != null) {
@@ -430,6 +441,11 @@ public class FileMenuFilter {
         return isSingleSelection() && MimeTypeUtil.isImage(mFiles.iterator().next());
     }
 
+    private boolean isSingleMedia() {
+        OCFile file = mFiles.iterator().next();
+        return isSingleSelection() && (MimeTypeUtil.isVideo(file) || MimeTypeUtil.isAudio(file));
+    }
+
     private boolean allFiles() {
         return mFiles != null && !containsFolder();
     }

+ 91 - 0
src/main/java/com/owncloud/android/files/StreamMediaFileOperation.java

@@ -0,0 +1,91 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2018 Tobias Kaminsky
+ * Copyright (C) 2018 Nextcloud GmbH.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.files;
+
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.operations.RemoteOperation;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
+import com.owncloud.android.lib.common.utils.Log_OC;
+
+import org.apache.commons.httpclient.HttpStatus;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+public class StreamMediaFileOperation extends RemoteOperation {
+    private static final String TAG = StreamMediaFileOperation.class.getSimpleName();
+    private static final int SYNC_READ_TIMEOUT = 40000;
+    private static final int SYNC_CONNECTION_TIMEOUT = 5000;
+    private static final String STREAM_MEDIA_URL = "/ocs/v2.php/apps/dav/api/v1/direct";
+    
+    private String fileID;
+
+    // JSON node names
+    private static final String NODE_OCS = "ocs";
+    private static final String NODE_DATA = "data";
+    private static final String NODE_URL = "url";
+    private static final String JSON_FORMAT = "?format=json";
+
+    public StreamMediaFileOperation(String fileID) {
+        this.fileID = fileID;
+    }
+
+    protected RemoteOperationResult run(OwnCloudClient client) {
+        RemoteOperationResult result;
+        PostMethod postMethod = null;
+
+        try {
+            postMethod = new PostMethod(client.getBaseUri() + STREAM_MEDIA_URL + JSON_FORMAT);
+            postMethod.setParameter("fileId", fileID);
+
+            // remote request
+            postMethod.addRequestHeader(OCS_API_HEADER, OCS_API_HEADER_VALUE);
+
+            int status = client.executeMethod(postMethod, SYNC_READ_TIMEOUT, SYNC_CONNECTION_TIMEOUT);
+
+            if (status == HttpStatus.SC_OK) {
+                String response = postMethod.getResponseBodyAsString();
+
+                // Parse the response
+                JSONObject respJSON = new JSONObject(response);
+                String url = respJSON.getJSONObject(NODE_OCS).getJSONObject(NODE_DATA).getString(NODE_URL);
+
+                result = new RemoteOperationResult(true, postMethod);
+                ArrayList<Object> urlArray = new ArrayList<>();
+                urlArray.add(url);
+                result.setData(urlArray);
+            } else {
+                result = new RemoteOperationResult(false, postMethod);
+                client.exhaustResponse(postMethod.getResponseBodyAsStream());
+            }
+        } catch (Exception e) {
+            result = new RemoteOperationResult(e);
+            Log_OC.e(TAG, "Get stream url for file with id " + fileID + " failed: " + result.getLogMessage(),
+                    result.getException());
+        } finally {
+            if (postMethod != null) {
+                postMethod.releaseConnection();
+            }
+        }
+        return result;
+    }
+}

+ 76 - 26
src/main/java/com/owncloud/android/media/MediaService.java

@@ -1,9 +1,13 @@
-/**
+/*
  * ownCloud Android client application
  *
  * @author David A. Velasco
  * Copyright (C) 2016 ownCloud Inc.
  *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2018 Tobias Kaminsky
+ * Copyright (C) 2018 Nextcloud GmbH.
+ *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License version 2,
  * as published by the Free Software Foundation.
@@ -20,6 +24,8 @@
 package com.owncloud.android.media;
 
 import android.accounts.Account;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.Service;
@@ -32,6 +38,7 @@ import android.media.MediaPlayer.OnErrorListener;
 import android.media.MediaPlayer.OnPreparedListener;
 import android.net.wifi.WifiManager;
 import android.net.wifi.WifiManager.WifiLock;
+import android.os.AsyncTask;
 import android.os.IBinder;
 import android.os.PowerManager;
 import android.support.v4.app.NotificationCompat;
@@ -39,6 +46,12 @@ import android.widget.Toast;
 
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.files.StreamMediaFileOperation;
+import com.owncloud.android.lib.common.OwnCloudAccount;
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
+import com.owncloud.android.lib.common.accounts.AccountUtils;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.ui.activity.FileActivity;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
@@ -46,6 +59,7 @@ import com.owncloud.android.ui.notifications.NotificationUtils;
 import com.owncloud.android.utils.ThemeUtils;
 
 import java.io.IOException;
+import java.lang.ref.WeakReference;
 
 
 /**
@@ -365,7 +379,6 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
         }
     }
 
-
     /**
      * Fully releases the audio focus.
      */
@@ -443,33 +456,19 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
 
             createMediaPlayerIfNeeded();
             mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
-            String url = mFile.getStoragePath();
-
-            /* Streaming is not possible right now
-            if (url == null || url.length() <= 0) {
-                url = AccountUtils.constructFullURLForAccount(this, mAccount) + mFile.getRemotePath();
-            }
-            mIsStreaming = url.startsWith("http:") || url.startsWith("https:");
-            */
-            //mIsStreaming = false;
-            mPlayer.setDataSource(url);
 
-            mState = State.PREPARING;
-            setUpAsForeground(String.format(getString(R.string.media_state_loading), mFile.getFileName()));
-
-            // starts preparing the media player in background
-            mPlayer.prepareAsync();
+            if (mFile.isDown()) {
+                mPlayer.setDataSource(mFile.getStoragePath());
+                preparePlayer();
+            } else {
+                OwnCloudAccount ocAccount = new OwnCloudAccount(mAccount, getBaseContext());
+                OwnCloudClient client = OwnCloudClientManagerFactory.getDefaultSingleton().
+                        getClientFor(ocAccount, getBaseContext());
 
-            // prevent the Wifi from going to sleep when streaming
-            /*
-            if (mIsStreaming) {
-                mWifiLock.acquire();
-            } else
-            */
-            if (mWifiLock.isHeld()) {
-                mWifiLock.release();
+                new LoadStreamUrl(this, client).execute(mFile.getLocalId());
             }
-
+        } catch (AccountUtils.AccountNotFoundException | OperationCanceledException | AuthenticatorException e) {
+            Log_OC.e(TAG, "Loading stream url not possible: " + e.getMessage());
         } catch (SecurityException | IOException | IllegalStateException | IllegalArgumentException e) {
             Log_OC.e(TAG, e.getClass().getSimpleName() + " playing " + mAccount.name + mFile.getRemotePath(), e);
             Toast.makeText(this, String.format(getString(R.string.media_err_playing), mFile.getFileName()),
@@ -478,6 +477,13 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
         }
     }
 
+    private void preparePlayer() {
+        mState = State.PREPARING;
+        setUpAsForeground(String.format(getString(R.string.media_state_loading), mFile.getFileName()));
+
+        // starts preparing the media player in background
+        mPlayer.prepareAsync();
+    }
 
     /** Called when media player is done playing current song. */
     public void onCompletion(MediaPlayer player) {
@@ -702,4 +708,48 @@ public class MediaService extends Service implements OnCompletionListener, OnPre
         return mMediaController;
     }
 
+    private static class LoadStreamUrl extends AsyncTask<String, Void, String> {
+
+        private OwnCloudClient client;
+        private WeakReference<MediaService> mediaServiceWeakReference;
+
+        public LoadStreamUrl(MediaService mediaService, OwnCloudClient client) {
+            this.client = client;
+            this.mediaServiceWeakReference = new WeakReference<>(mediaService);
+        }
+
+        @Override
+        protected String doInBackground(String... fileId) {
+            StreamMediaFileOperation sfo = new StreamMediaFileOperation(fileId[0]);
+            RemoteOperationResult result = sfo.execute(client);
+
+            if (!result.isSuccess()) {
+                return null;
+            }
+
+            return (String) result.getData().get(0);
+        }
+
+        @Override
+        protected void onPostExecute(String url) {
+            MediaService mediaService = mediaServiceWeakReference.get();
+
+            if (mediaService != null) {
+                if (url != null) {
+                    try {
+                        mediaService.mPlayer.setDataSource(url);
+
+                        // prevent the Wifi from going to sleep when streaming
+                        mediaService.mWifiLock.acquire();
+                        mediaService.preparePlayer();
+                    } catch (IOException e) {
+                        Log_OC.e(TAG, "Streaming not possible: " + e.getMessage());
+                    }
+                } else {
+                    // we already show a toast with error from media player
+                    mediaService.processStopRequest(true);
+                }
+            }
+        }
+    }
 }

+ 15 - 8
src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -725,11 +725,13 @@ public class FileDisplayActivity extends HookActivity
                 boolean detailsFragmentChanged = false;
                 if (waitedPreview) {
                     if (success) {
-                        mWaitingToPreview = getStorageManager().getFileById(
-                                mWaitingToPreview.getFileId());   // update the file from database,
-                        // for the local storage path
+                        // update the file from database, for the local storage path
+                        mWaitingToPreview = getStorageManager().getFileById(mWaitingToPreview.getFileId());
+                        
                         if (PreviewMediaFragment.canBePreviewed(mWaitingToPreview)) {
-                            startMediaPreview(mWaitingToPreview, 0, true, true);
+                            boolean streaming = AccountUtils.getServerVersionForAccount(getAccount(), this)
+                                    .isMediaStreamingSupported();
+                            startMediaPreview(mWaitingToPreview, 0, true, true, streaming);
                             detailsFragmentChanged = true;
                         } else if (MimeTypeUtil.isVCard(mWaitingToPreview.getMimeType())) {
                             startContactListFragment(mWaitingToPreview);
@@ -2050,7 +2052,9 @@ public class FileDisplayActivity extends HookActivity
                     ((PreviewMediaFragment) details).updateFile(renamedFile);
                     if (PreviewMediaFragment.canBePreviewed(renamedFile)) {
                         int position = ((PreviewMediaFragment) details).getPosition();
-                        startMediaPreview(renamedFile, position, true, true);
+                        boolean streaming = AccountUtils.getServerVersionForAccount(getAccount(), this)
+                                .isMediaStreamingSupported();
+                        startMediaPreview(renamedFile, position, true, true, streaming);
                     } else {
                         getFileOperationsHelper().openFile(renamedFile);
                     }
@@ -2323,8 +2327,9 @@ public class FileDisplayActivity extends HookActivity
      * @param autoplay              When 'true', the playback will start without user
      *                              interactions.
      */
-    public void startMediaPreview(OCFile file, int startPlaybackPosition, boolean autoplay, boolean showPreview) {
-        if (showPreview && file.isDown() && !file.isDownloading()) {
+    public void startMediaPreview(OCFile file, int startPlaybackPosition, boolean autoplay, boolean showPreview,
+                                  boolean streamMedia) {
+        if (showPreview && file.isDown() && !file.isDownloading() || streamMedia) {
             Fragment mediaFragment = PreviewMediaFragment.newInstance(file, getAccount(), startPlaybackPosition, autoplay);
             setSecondFragment(mediaFragment);
             updateFragmentsVisibility(true);
@@ -2459,9 +2464,11 @@ public class FileDisplayActivity extends HookActivity
         if (event.getIntent().getBooleanExtra(TEXT_PREVIEW, false)) {
             startTextPreview((OCFile) bundle.get(EXTRA_FILE), true);
         } else if (bundle.containsKey(PreviewVideoActivity.EXTRA_START_POSITION)) {
+            boolean streaming = AccountUtils.getServerVersionForAccount(getAccount(), this)
+                    .isMediaStreamingSupported();
             startMediaPreview((OCFile)bundle.get(EXTRA_FILE),
                     (int)bundle.get(PreviewVideoActivity.EXTRA_START_POSITION),
-                    (boolean)bundle.get(PreviewVideoActivity.EXTRA_AUTOPLAY), true);
+                    (boolean) bundle.get(PreviewVideoActivity.EXTRA_AUTOPLAY), true, streaming);
         } else if (bundle.containsKey(PreviewImageActivity.EXTRA_VIRTUAL_TYPE)) {
             startImagePreview((OCFile)bundle.get(EXTRA_FILE),
                     (VirtualFolderType)bundle.get(PreviewImageActivity.EXTRA_VIRTUAL_TYPE),

+ 45 - 38
src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java

@@ -813,48 +813,51 @@ public class OCFileListFragment extends ExtendedListFragment implements
                     saveIndexAndTopPosition(position);
                 }
 
-            } else if (!mOnlyFoldersClickable){ // Click on a file
-                if (PreviewImageFragment.canBePreviewed(file)) {
-                    // preview image - it handles the download, if needed
-                    if (searchFragment) {
-                        VirtualFolderType type;
-                        switch (currentSearchType) {
-                            case FAVORITE_SEARCH:
-                                type = VirtualFolderType.FAVORITE;
-                                break;
-                            case PHOTO_SEARCH:
-                                type = VirtualFolderType.PHOTOS;
-                                break;
-                            default:
-                                type = VirtualFolderType.NONE;
-                                break;
+                } else if (!mOnlyFoldersClickable) { // Click on a file
+                    if (PreviewImageFragment.canBePreviewed(file)) {
+                        // preview image - it handles the download, if needed
+                        if (searchFragment) {
+                            VirtualFolderType type;
+                            switch (currentSearchType) {
+                                case FAVORITE_SEARCH:
+                                    type = VirtualFolderType.FAVORITE;
+                                    break;
+                                case PHOTO_SEARCH:
+                                    type = VirtualFolderType.PHOTOS;
+                                    break;
+                                default:
+                                    type = VirtualFolderType.NONE;
+                                    break;
+                            }
+                            ((FileDisplayActivity) mContainerActivity).startImagePreview(file, type, !file.isDown());
+                        } else {
+                            ((FileDisplayActivity) mContainerActivity).startImagePreview(file, !file.isDown());
+                        }
+                    } else if (file.isDown() && MimeTypeUtil.isVCard(file)) {
+                        ((FileDisplayActivity) mContainerActivity).startContactListFragment(file);
+                    } else if (PreviewTextFragment.canBePreviewed(file)) {
+                        ((FileDisplayActivity) mContainerActivity).startTextPreview(file, false);
+                    } else if (file.isDown()) {
+                        if (PreviewMediaFragment.canBePreviewed(file)) {
+                            // media preview
+                            ((FileDisplayActivity) mContainerActivity).startMediaPreview(file, 0, true, true, false);
+                        } else {
+                            mContainerActivity.getFileOperationsHelper().openFile(file);
                         }
-                        ((FileDisplayActivity) mContainerActivity).startImagePreview(file, type, !file.isDown());
-                    } else {
-                        ((FileDisplayActivity) mContainerActivity).startImagePreview(file, !file.isDown());
-                    }
-                } else if (file.isDown() && MimeTypeUtil.isVCard(file)) {
-                    ((FileDisplayActivity) mContainerActivity).startContactListFragment(file);
-                } else if (PreviewTextFragment.canBePreviewed(file)) {
-                    ((FileDisplayActivity) mContainerActivity).startTextPreview(file, false);
-                } else if (file.isDown()) {
-                    if (PreviewMediaFragment.canBePreviewed(file)) {
-                        // media preview
-                        ((FileDisplayActivity) mContainerActivity).startMediaPreview(file, 0,
-                                true, false);
                     } else {
-                        mContainerActivity.getFileOperationsHelper().openFile(file);
+                        if (PreviewMediaFragment.canBePreviewed(file) && AccountUtils.getServerVersion(
+                                AccountUtils.getCurrentOwnCloudAccount(getContext())).isMediaStreamingSupported()) {
+                            // stream media preview on >= NC14
+                            ((FileDisplayActivity) mContainerActivity).startMediaPreview(file, 0, true, true, true);
+                        } else {
+                            // automatic download, preview on finish
+                            ((FileDisplayActivity) mContainerActivity).startDownloadForPreview(file);
+                        }
                     }
-
-                } else {
-                    // automatic download, preview on finish
-                    ((FileDisplayActivity) mContainerActivity).startDownloadForPreview(file);
-                
-
-            }
-}
+                }
         } else {
-            Log_OC.d(TAG, "Null object in ListAdapter!!");}
+                Log_OC.d(TAG, "Null object in ListAdapter!");
+            }
         }
     }
 
@@ -903,6 +906,10 @@ public class OCFileListFragment extends ExtendedListFragment implements
                     mContainerActivity.getFileOperationsHelper().openFile(singleFile);
                     return true;
                 }
+                case R.id.action_stream_media: {
+                    mContainerActivity.getFileOperationsHelper().streamMediaFile(singleFile);
+                    return true;
+                }
                 case R.id.action_rename_file: {
                     RenameFileDialogFragment dialog = RenameFileDialogFragment.newInstance(singleFile);
                     dialog.show(getFragmentManager(), FileDetailFragment.FTAG_RENAME_FILE);

+ 26 - 0
src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java

@@ -48,6 +48,7 @@ import com.owncloud.android.R;
 import com.owncloud.android.authentication.AccountUtils;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.files.StreamMediaFileOperation;
 import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
@@ -348,6 +349,31 @@ public class FileOperationsHelper {
         }
     }
 
+    public void streamMediaFile(OCFile file) {
+        mFileActivity.showLoadingDialog(mFileActivity.getString(R.string.wait_a_moment));
+
+        new Thread(() -> {
+            Account account = AccountUtils.getCurrentOwnCloudAccount(mFileActivity);
+            StreamMediaFileOperation sfo = new StreamMediaFileOperation(file.getLocalId());
+            RemoteOperationResult result = sfo.execute(account, mFileActivity);
+
+            mFileActivity.dismissLoadingDialog();
+
+            if (!result.isSuccess()) {
+                DisplayUtils.showSnackMessage(mFileActivity, R.string.stream_not_possible_headline);
+                return;
+            }
+
+            Intent openFileWithIntent = new Intent(Intent.ACTION_VIEW);
+            Uri uri = Uri.parse((String) result.getData().get(0));
+
+            openFileWithIntent.setDataAndType(uri, file.getMimeType());
+
+            mFileActivity.startActivity(Intent.createChooser(openFileWithIntent,
+                    mFileActivity.getString(R.string.stream)));
+        }).start();
+    }
+
     /**
      * Helper method to share a file via a public link. Starts a request to do it in {@link OperationsService}
      *

+ 73 - 17
src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java

@@ -34,6 +34,8 @@ import android.media.MediaPlayer;
 import android.media.MediaPlayer.OnCompletionListener;
 import android.media.MediaPlayer.OnErrorListener;
 import android.media.MediaPlayer.OnPreparedListener;
+import android.net.Uri;
+import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.support.annotation.DrawableRes;
@@ -58,6 +60,11 @@ import android.widget.VideoView;
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.files.FileMenuFilter;
+import com.owncloud.android.files.StreamMediaFileOperation;
+import com.owncloud.android.lib.common.OwnCloudAccount;
+import com.owncloud.android.lib.common.OwnCloudClient;
+import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
+import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.media.MediaControlView;
 import com.owncloud.android.media.MediaService;
@@ -69,6 +76,8 @@ import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment;
 import com.owncloud.android.ui.fragment.FileFragment;
 import com.owncloud.android.utils.MimeTypeUtil;
 
+import java.lang.ref.WeakReference;
+
 
 /**
  * This fragment shows a preview of a downloaded media file (audio or video).
@@ -108,6 +117,8 @@ public class PreviewMediaFragment extends FileFragment implements
     private static boolean mOnResume = false;
     public boolean mPrepared;
 
+    private Uri mVideoUri;
+
     private static final String TAG = PreviewMediaFragment.class.getSimpleName();
 
     private static final String FILE = "FILE";
@@ -221,6 +232,7 @@ public class PreviewMediaFragment extends FileFragment implements
             mMultiListMessage.setText(message);
             mMultiListIcon.setImageResource(icon);
 
+            mMultiListMessage.setVisibility(View.VISIBLE);
             mMultiListIcon.setVisibility(View.VISIBLE);
             mMultiListProgress.setVisibility(View.GONE);
         }
@@ -244,19 +256,15 @@ public class PreviewMediaFragment extends FileFragment implements
             if (mAccount == null) {
                 throw new IllegalStateException("Instanced with a NULL ownCloud Account");
             }
-            if (!file.isDown()) {
-                throw new IllegalStateException("There is no local file to preview");
-            }
         } else {
             file = savedInstanceState.getParcelable(PreviewMediaFragment.EXTRA_FILE);
             setFile(file);
             mAccount = savedInstanceState.getParcelable(PreviewMediaFragment.EXTRA_ACCOUNT);
             mSavedPlaybackPosition = savedInstanceState.getInt(PreviewMediaFragment.EXTRA_PLAY_POSITION);
             mAutoplay = savedInstanceState.getBoolean(PreviewMediaFragment.EXTRA_PLAYING);
-
         }
 
-        if (file != null && file.isDown()) {
+        if (file != null) {
             if (MimeTypeUtil.isVideo(file)) {
                 mVideoPreview.setVisibility(View.VISIBLE);
                 mImagePreview.setVisibility(View.GONE);
@@ -327,16 +335,12 @@ public class PreviewMediaFragment extends FileFragment implements
         Log_OC.v(TAG, "onStart");
 
         OCFile file = getFile();
-        if (file != null && file.isDown()) {
+        if (file != null) {
             if (MimeTypeUtil.isAudio(file)) {
                 bindMediaService();
-
-            }
-            else {
-                if (MimeTypeUtil.isVideo(file)) {
-                    stopAudio();
-                    playVideo();
-                }
+            } else if (MimeTypeUtil.isVideo(file)) {
+                stopAudio();
+                playVideo();
             }
         }
     }
@@ -492,16 +496,68 @@ public class PreviewMediaFragment extends FileFragment implements
         mVideoPreview.setOnErrorListener(videoHelper);
     }
 
-    @SuppressWarnings("static-access")
     private void playVideo() {
         // create and prepare control panel for the user
         mMediaController.setMediaPlayer(mVideoPreview);
 
-        // load the video file in the video player ; 
+        // load the video file in the video player 
         // when done, VideoHelper#onPrepared() will be called
-        mVideoPreview.setVideoURI(getFile().getStorageUri());
+        if (getFile().isDown()) {
+            mVideoPreview.setVideoURI(getFile().getStorageUri());
+        } else {
+            try {
+                OwnCloudAccount ocAccount = new OwnCloudAccount(mAccount, getContext());
+                OwnCloudClient client = OwnCloudClientManagerFactory.getDefaultSingleton().
+                        getClientFor(ocAccount, getContext());
+
+                new LoadStreamUrl(this, client).execute(getFile().getLocalId());
+            } catch (Exception e) {
+                Log_OC.e(TAG, "Loading stream url not possible: " + e);
+            }
+        }
     }
 
+    private static class LoadStreamUrl extends AsyncTask<String, Void, Uri> {
+
+        private OwnCloudClient client;
+        private WeakReference<PreviewMediaFragment> previewMediaFragmentWeakReference;
+
+        public LoadStreamUrl(PreviewMediaFragment previewMediaFragment, OwnCloudClient client) {
+            this.client = client;
+            this.previewMediaFragmentWeakReference = new WeakReference<>(previewMediaFragment);
+        }
+
+        @Override
+        protected Uri doInBackground(String... fileId) {
+            StreamMediaFileOperation sfo = new StreamMediaFileOperation(fileId[0]);
+            RemoteOperationResult result = sfo.execute(client);
+
+            if (!result.isSuccess()) {
+                return null;
+            }
+
+            return Uri.parse((String) result.getData().get(0));
+        }
+
+        @Override
+        protected void onPostExecute(Uri uri) {
+            PreviewMediaFragment previewMediaFragment = previewMediaFragmentWeakReference.get();
+
+            if (previewMediaFragment != null) {
+                if (uri != null) {
+                    previewMediaFragment.mVideoUri = uri;
+                    previewMediaFragment.mVideoPreview.setVideoURI(uri);
+                } else {
+                    previewMediaFragment.mMultiView.setVisibility(View.VISIBLE);
+                    previewMediaFragment.setMessageForMultiList(
+                            previewMediaFragment.getString(R.string.stream_not_possible_headline),
+                            R.string.stream_not_possible_message, R.drawable.file_movie);
+                }
+            } else {
+                Log_OC.e(TAG, "Error streaming file: no previewMediaFragment!");
+            }
+        }
+    }
 
     private class VideoHelper implements OnCompletionListener, OnPreparedListener, OnErrorListener {
 
@@ -624,6 +680,7 @@ public class PreviewMediaFragment extends FileFragment implements
         i.putExtra(FileActivity.EXTRA_ACCOUNT, mAccount);
         i.putExtra(FileActivity.EXTRA_FILE, getFile());
         i.putExtra(PreviewVideoActivity.EXTRA_AUTOPLAY, mVideoPreview.isPlaying());
+        i.putExtra(PreviewVideoActivity.EXTRA_STREAM_URL, mVideoUri);
         mVideoPreview.pause();
         i.putExtra(PreviewVideoActivity.EXTRA_START_POSITION, mVideoPreview.getCurrentPosition());
         startActivityForResult(i, FileActivity.REQUEST_CODE__LAST_SHARED + 1);
@@ -652,7 +709,6 @@ public class PreviewMediaFragment extends FileFragment implements
         if (!mMediaServiceBinder.isPlaying(file) && !mOnResume) {
             Log_OC.d(TAG, "starting playback of " + file.getStoragePath());
             mMediaServiceBinder.start(mAccount, file, mAutoplay, mSavedPlaybackPosition);
-
         }
         else {
             if (!mMediaServiceBinder.isPlaying() && mAutoplay) {

+ 8 - 13
src/main/java/com/owncloud/android/ui/preview/PreviewVideoActivity.java

@@ -35,8 +35,6 @@ import android.widget.VideoView;
 
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.lib.common.accounts.AccountUtils;
-import com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.media.MediaService;
 import com.owncloud.android.ui.activity.FileActivity;
@@ -57,6 +55,8 @@ public class PreviewVideoActivity extends FileActivity implements OnCompletionLi
     
     /** Key to receive the position of the playback where the video should be put at start */
     public static final String EXTRA_START_POSITION = "START_POSITION";
+
+    public static final String EXTRA_STREAM_URL = "STREAM_URL";
     
     private static final String TAG = PreviewVideoActivity.class.getSimpleName();
 
@@ -64,6 +64,7 @@ public class PreviewVideoActivity extends FileActivity implements OnCompletionLi
     private boolean mAutoplay;                  // when 'true', the playback starts immediately with the activity
     private VideoView mVideoPlayer;             // view to play the file; both performs and show the playback
     private MediaController mMediaController;   // panel control used by the user to control the playback
+    private Uri mStreamUri;
           
     /** 
      *  Called when the activity is first created.
@@ -86,15 +87,16 @@ public class PreviewVideoActivity extends FileActivity implements OnCompletionLi
             Bundle extras = getIntent().getExtras();
             mSavedPlaybackPosition = extras.getInt(EXTRA_START_POSITION);
             mAutoplay = extras.getBoolean(EXTRA_AUTOPLAY);
-            
+            mStreamUri = (Uri) extras.get(EXTRA_STREAM_URL);
         } else {
             mSavedPlaybackPosition = savedInstanceState.getInt(EXTRA_START_POSITION);
             mAutoplay = savedInstanceState.getBoolean(EXTRA_AUTOPLAY);
+            mStreamUri = (Uri) savedInstanceState.get(EXTRA_STREAM_URL);
         }
           
         mVideoPlayer = (VideoView) findViewById(R.id.videoPlayer);
 
-        // set listeners to get more contol on the playback
+        // set listeners to get more control on the playback
         mVideoPlayer.setOnPreparedListener(this);
         mVideoPlayer.setOnCompletionListener(this);
         mVideoPlayer.setOnErrorListener(this);
@@ -115,6 +117,7 @@ public class PreviewVideoActivity extends FileActivity implements OnCompletionLi
         super.onSaveInstanceState(outState);
         outState.putInt(PreviewVideoActivity.EXTRA_START_POSITION, mVideoPlayer.getCurrentPosition());
         outState.putBoolean(PreviewVideoActivity.EXTRA_AUTOPLAY , mVideoPlayer.isPlaying());
+        outState.putParcelable(PreviewVideoActivity.EXTRA_STREAM_URL, mStreamUri);
     }
 
     
@@ -207,16 +210,8 @@ public class PreviewVideoActivity extends FileActivity implements OnCompletionLi
             if (file != null) {
                 if (file.isDown()) {
                     mVideoPlayer.setVideoURI(file.getStorageUri());
-
                 } else {
-                    // not working yet
-                    String url;
-                    try {
-                        url = AccountUtils.constructFullURLForAccount(this, getAccount()) + file.getRemotePath();
-                        mVideoPlayer.setVideoURI(Uri.parse(url));
-                    } catch (AccountNotFoundException e) {
-                        onError(null, MediaService.OC_MEDIA_ERROR, R.string.media_err_no_account);
-                    }
+                    mVideoPlayer.setVideoURI(mStreamUri);
                 }
 
                 // create and prepare control panel for the user

+ 0 - 1
src/main/res/layout/empty_list.xml

@@ -62,7 +62,6 @@
         android:layout_gravity="center_horizontal"
         android:ellipsize="end"
         android:gravity="center"
-        android:maxLines="3"
         android:text="@string/file_list_empty"
         android:visibility="gone"/>
 

+ 7 - 0
src/main/res/menu/file_actions_menu.xml

@@ -64,6 +64,13 @@
         app:showAsAction="never"
         android:showAsAction="never" />
 
+    <item
+        android:id="@+id/action_stream_media"
+        android:title="@string/stream"
+        app:showAsAction="never"
+        android:showAsAction="never"
+        android:orderInCategory="1"/>
+    
     <item
         android:id="@+id/action_send_share_file"
         android:title="@string/action_send_share"

+ 3 - 1
src/main/res/values/strings.xml

@@ -223,7 +223,6 @@
     <string name="media_state_loading">%1$s (loading)</string>
     <string name="media_event_done">%1$s playback finished</string>
     <string name="media_err_nothing_to_play">No media file found</string>
-    <string name="media_err_no_account">No account provided</string>
     <string name="media_err_not_in_owncloud">The file is not in a valid account</string>
     <string name="media_err_unsupported">Unsupported media codec</string>
     <string name="media_err_io">Could not read the media file</string>
@@ -823,4 +822,7 @@
     <string name="trashbin_file_not_deleted">File %1$s could not be deleted!</string>
     <string name="trashbin_file_not_restored">File %1$s could not be restored!</string>
     <string name="trashbin_not_emptied">Files could not be deleted permanently!</string>
+    <string name="stream">Stream with…</string>
+    <string name="stream_not_possible_headline">Internal streaming not possible</string>
+    <string name="stream_not_possible_message">Please download media instead or use external app.</string>
 </resources>