From 853115eb4e2f564de770063ae4683980fe3475e9 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Tue, 5 Jan 2021 22:10:54 -0500 Subject: [PATCH] live video (untested) #383; also besties story type #460 --- app/build.gradle | 6 +- .../viewholder/FeedStoryViewHolder.java | 4 + .../customviews/CircularImageView.java | 7 +- .../fragments/HashTagFragment.java | 2 +- .../fragments/LocationFragment.java | 2 +- .../fragments/StoryViewerFragment.java | 129 ++++++++++++++---- .../fragments/main/ProfileFragment.java | 2 +- .../instagrabber/models/FeedStoryModel.java | 14 +- .../models/enums/MediaItemType.java | 5 +- .../instagrabber/utils/ResponseBodyUtils.java | 13 ++ .../webservices/StoriesService.java | 24 +++- 11 files changed, 167 insertions(+), 41 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 454e38ad..259b7149 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -60,11 +60,13 @@ dependencies { def appcompat_version = "1.2.0" def nav_version = '2.3.2' + def exoplayer_version = '2.12.0' implementation 'com.google.android.material:material:1.3.0-alpha04' - implementation 'com.google.android.exoplayer:exoplayer-core:2.12.0' - implementation 'com.google.android.exoplayer:exoplayer-ui:2.12.0' + implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version" + implementation "com.google.android.exoplayer:exoplayer-dash:$exoplayer_version" + implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version" implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "androidx.appcompat:appcompat-resources:$appcompat_version" diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedStoryViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedStoryViewHolder.java index 76b309e9..7db15277 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedStoryViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/FeedStoryViewHolder.java @@ -33,5 +33,9 @@ public final class FeedStoryViewHolder extends RecyclerView.ViewHolder { binding.title.setAlpha(model.isFullyRead() ? 0.5F : 1.0F); binding.icon.setImageURI(profileModel.getSdProfilePic()); binding.icon.setAlpha(model.isFullyRead() ? 0.5F : 1.0F); + + if (model.isLive()) binding.icon.setStoriesBorder(2); + else if (model.isBestie()) binding.icon.setStoriesBorder(1); + else binding.icon.setStoriesBorder(0); } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/customviews/CircularImageView.java b/app/src/main/java/awais/instagrabber/customviews/CircularImageView.java index 29828709..2f687868 100755 --- a/app/src/main/java/awais/instagrabber/customviews/CircularImageView.java +++ b/app/src/main/java/awais/instagrabber/customviews/CircularImageView.java @@ -49,14 +49,15 @@ public class CircularImageView extends SimpleDraweeView { setBackgroundResource(R.drawable.shape_oval_light); } - public void setStoriesBorder() { + /* types: 0 clear, 1 green (feed bestie / has story), 2 red (live) */ + public void setStoriesBorder(final int type) { // private final int borderSize = 8; - final int color = Color.GREEN; + final int color = type == 2 ? Color.RED : Color.GREEN; RoundingParams roundingParams = getHierarchy().getRoundingParams(); if (roundingParams == null) { roundingParams = RoundingParams.asCircle().setRoundingMethod(RoundingParams.RoundingMethod.BITMAP_ONLY); } - roundingParams.setBorder(color, 5.0f); + roundingParams.setBorder(color, type == 0 ? 0f : 5.0f); getHierarchy().setRoundingParams(roundingParams); } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java b/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java index 10fc0482..bdec3b59 100644 --- a/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/HashTagFragment.java @@ -580,7 +580,7 @@ public class HashTagFragment extends Fragment implements SwipeRefreshLayout.OnRe @Override public void onSuccess(final List storyModels) { if (storyModels != null && !storyModels.isEmpty()) { - hashtagDetailsBinding.mainHashtagImage.setStoriesBorder(); + hashtagDetailsBinding.mainHashtagImage.setStoriesBorder(1); hasStories = true; } else { hasStories = false; diff --git a/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java b/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java index c48d4939..0d464748 100644 --- a/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/LocationFragment.java @@ -549,7 +549,7 @@ public class LocationFragment extends Fragment implements SwipeRefreshLayout.OnR @Override public void onSuccess(final List storyModels) { if (storyModels != null && !storyModels.isEmpty()) { - locationDetailsBinding.mainLocationImage.setStoriesBorder(); + locationDetailsBinding.mainLocationImage.setStoriesBorder(1); hasStories = true; } storiesFetching = false; diff --git a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java index c19d584b..3fc533f7 100644 --- a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java @@ -50,6 +50,7 @@ import com.facebook.imagepipeline.request.ImageRequestBuilder; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaSource; @@ -643,6 +644,7 @@ public class StoryViewerFragment extends Fragment { private void resetView() { final Context context = getContext(); + StoryModel live = null; slidePos = 0; lastSlidePos = 0; if (menuDownload != null) menuDownload.setVisible(false); @@ -679,6 +681,7 @@ public class StoryViewerFragment extends Fragment { final FeedStoryModel model = models.get(currentFeedStoryIndex); currentStoryMediaId = model.getStoryMediaId(); currentStoryUsername = model.getProfileModel().getUsername(); + if (model.isLive()) live = model.getFirstStoryModel(); } } else if (!TextUtils.isEmpty(fragmentArgs.getProfileId()) && !TextUtils.isEmpty(fragmentArgs.getUsername())) { currentStoryMediaId = fragmentArgs.getProfileId(); @@ -756,7 +759,8 @@ public class StoryViewerFragment extends Fragment { Log.e(TAG, "Error", t); } }; - storiesService.getUserStory(currentStoryMediaId, + if (live != null) storyCallback.onSuccess(Collections.singletonList(live)); + else storiesService.getUserStory(currentStoryMediaId, currentStoryUsername, isLoc, isHashtag, @@ -786,41 +790,43 @@ public class StoryViewerFragment extends Fragment { final MediaItemType itemType = currentStory.getItemType(); if (menuDownload != null) menuDownload.setVisible(false); - url = itemType == MediaItemType.MEDIA_TYPE_VIDEO ? currentStory.getVideoUrl() : currentStory.getStoryUrl(); + url = itemType == MediaItemType.MEDIA_TYPE_IMAGE ? currentStory.getStoryUrl() : currentStory.getVideoUrl(); - final String shortCode = currentStory.getTappableShortCode(); - binding.viewStoryPost.setVisibility(shortCode != null ? View.VISIBLE : View.GONE); - binding.viewStoryPost.setTag(shortCode); + if (itemType != MediaItemType.MEDIA_TYPE_LIVE) { + final String shortCode = currentStory.getTappableShortCode(); + binding.viewStoryPost.setVisibility(shortCode != null ? View.VISIBLE : View.GONE); + binding.viewStoryPost.setTag(shortCode); - final String spotify = currentStory.getSpotify(); - binding.spotify.setVisibility(spotify != null ? View.VISIBLE : View.GONE); - binding.spotify.setTag(spotify); + final String spotify = currentStory.getSpotify(); + binding.spotify.setVisibility(spotify != null ? View.VISIBLE : View.GONE); + binding.spotify.setTag(spotify); - poll = currentStory.getPoll(); - binding.poll.setVisibility(poll != null ? View.VISIBLE : View.GONE); - binding.poll.setTag(poll); + poll = currentStory.getPoll(); + binding.poll.setVisibility(poll != null ? View.VISIBLE : View.GONE); + binding.poll.setTag(poll); - question = currentStory.getQuestion(); - binding.answer.setVisibility((question != null && !TextUtils.isEmpty(cookie)) ? View.VISIBLE : View.GONE); - binding.answer.setTag(question); + question = currentStory.getQuestion(); + binding.answer.setVisibility((question != null && !TextUtils.isEmpty(cookie)) ? View.VISIBLE : View.GONE); + binding.answer.setTag(question); - mentions = currentStory.getMentions(); - binding.mention.setVisibility((mentions != null && mentions.length > 0) ? View.VISIBLE : View.GONE); - binding.mention.setTag(mentions); + mentions = currentStory.getMentions(); + binding.mention.setVisibility((mentions != null && mentions.length > 0) ? View.VISIBLE : View.GONE); + binding.mention.setTag(mentions); - quiz = currentStory.getQuiz(); - binding.quiz.setVisibility(quiz != null ? View.VISIBLE : View.GONE); - binding.quiz.setTag(quiz); + quiz = currentStory.getQuiz(); + binding.quiz.setVisibility(quiz != null ? View.VISIBLE : View.GONE); + binding.quiz.setTag(quiz); - slider = currentStory.getSlider(); - binding.slider.setVisibility(slider != null ? View.VISIBLE : View.GONE); - binding.slider.setTag(slider); + slider = currentStory.getSlider(); + binding.slider.setVisibility(slider != null ? View.VISIBLE : View.GONE); + binding.slider.setTag(slider); - swipeUp = currentStory.getSwipeUp(); - if (swipeUp != null) { - binding.swipeUp.setVisibility(View.VISIBLE); - binding.swipeUp.setText(swipeUp.getText()); - binding.swipeUp.setTag(swipeUp.getUrl()); + swipeUp = currentStory.getSwipeUp(); + if (swipeUp != null) { + binding.swipeUp.setVisibility(View.VISIBLE); + binding.swipeUp.setText(swipeUp.getText()); + binding.swipeUp.setTag(swipeUp.getUrl()); + } else binding.swipeUp.setVisibility(View.GONE); } releasePlayer(); @@ -832,6 +838,7 @@ public class StoryViewerFragment extends Fragment { } } if (itemType == MediaItemType.MEDIA_TYPE_VIDEO) setupVideo(); + else if (itemType == MediaItemType.MEDIA_TYPE_LIVE) setupLive(); else setupImage(); final ActionBar actionBar = fragmentActivity.getSupportActionBar(); @@ -957,6 +964,72 @@ public class StoryViewerFragment extends Fragment { }); } + private void setupLive() { + binding.playerView.setVisibility(View.VISIBLE); + binding.progressView.setVisibility(View.GONE); + binding.imageViewer.setVisibility(View.GONE); + binding.imageViewer.setController(null); + + if (menuDownload != null) menuDownload.setVisible(false); + if (menuDm != null) menuDm.setVisible(false); + + final Context context = getContext(); + if (context == null) return; + player = new SimpleExoPlayer.Builder(context).build(); + binding.playerView.setPlayer(player); + player.setPlayWhenReady(settingsHelper.getBoolean(Constants.AUTOPLAY_VIDEOS)); + + final Uri uri = Uri.parse(url); + final MediaItem mediaItem = MediaItem.fromUri(uri); + final DashMediaSource mediaSource = new DashMediaSource.Factory(new DefaultDataSourceFactory(context, "instagram")) + .createMediaSource(mediaItem); + mediaSource.addEventListener(new Handler(), new MediaSourceEventListener() { + @Override + public void onLoadCompleted(final int windowIndex, + @Nullable final MediaSource.MediaPeriodId mediaPeriodId, + final LoadEventInfo loadEventInfo, + final MediaLoadData mediaLoadData) { + binding.progressView.setVisibility(View.GONE); + } + + @Override + public void onLoadStarted(final int windowIndex, + @Nullable final MediaSource.MediaPeriodId mediaPeriodId, + final LoadEventInfo loadEventInfo, + final MediaLoadData mediaLoadData) { + binding.progressView.setVisibility(View.VISIBLE); + } + + @Override + public void onLoadCanceled(final int windowIndex, + @Nullable final MediaSource.MediaPeriodId mediaPeriodId, + final LoadEventInfo loadEventInfo, + final MediaLoadData mediaLoadData) { + binding.progressView.setVisibility(View.GONE); + } + + @Override + public void onLoadError(final int windowIndex, + @Nullable final MediaSource.MediaPeriodId mediaPeriodId, + final LoadEventInfo loadEventInfo, + final MediaLoadData mediaLoadData, + final IOException error, + final boolean wasCanceled) { + binding.progressView.setVisibility(View.GONE); + } + }); + player.setMediaSource(mediaSource); + player.prepare(); + + binding.playerView.setOnClickListener(v -> { + if (player != null) { + if (player.getPlaybackState() == Player.STATE_ENDED) player.seekTo(0); + player.setPlayWhenReady(player.getPlaybackState() == Player.STATE_ENDED || !player.isPlaying()); + } + }); + } + + private void openProfile(final String username) { final ActionBar actionBar = fragmentActivity.getSupportActionBar(); if (actionBar != null) { diff --git a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java index d5ab821d..d5a85e34 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java @@ -869,7 +869,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe @Override public void onSuccess(final List storyModels) { if (storyModels != null && !storyModels.isEmpty()) { - profileDetailsBinding.mainProfileImage.setStoriesBorder(); + profileDetailsBinding.mainProfileImage.setStoriesBorder(1); hasStories = true; } } diff --git a/app/src/main/java/awais/instagrabber/models/FeedStoryModel.java b/app/src/main/java/awais/instagrabber/models/FeedStoryModel.java index 01ffac76..74bac68e 100755 --- a/app/src/main/java/awais/instagrabber/models/FeedStoryModel.java +++ b/app/src/main/java/awais/instagrabber/models/FeedStoryModel.java @@ -14,17 +14,21 @@ public final class FeedStoryModel implements Serializable { private final ProfileModel profileModel; private final StoryModel firstStoryModel; private Boolean fullyRead; + private final boolean isLive, isBestie; private final long timestamp; private final int mediaCount; public FeedStoryModel(final String storyMediaId, final ProfileModel profileModel, final boolean fullyRead, - final long timestamp, final StoryModel firstStoryModel, final int mediaCount) { + final long timestamp, final StoryModel firstStoryModel, final int mediaCount, + final boolean isLive, final boolean isBestie) { this.storyMediaId = storyMediaId; this.profileModel = profileModel; this.fullyRead = fullyRead; this.timestamp = timestamp; this.firstStoryModel = firstStoryModel; this.mediaCount = mediaCount; + this.isLive = isLive; + this.isBestie = isBestie; } public String getStoryMediaId() { @@ -63,4 +67,12 @@ public final class FeedStoryModel implements Serializable { public void setFullyRead(final boolean fullyRead) { this.fullyRead = fullyRead; } + + public boolean isLive() { + return isLive; + } + + public boolean isBestie() { + return isBestie; + } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/enums/MediaItemType.java b/app/src/main/java/awais/instagrabber/models/enums/MediaItemType.java index 727b0a1b..681926a5 100755 --- a/app/src/main/java/awais/instagrabber/models/enums/MediaItemType.java +++ b/app/src/main/java/awais/instagrabber/models/enums/MediaItemType.java @@ -8,10 +8,11 @@ public enum MediaItemType implements Serializable { MEDIA_TYPE_IMAGE(1), MEDIA_TYPE_VIDEO(2), MEDIA_TYPE_SLIDER(3), - MEDIA_TYPE_VOICE(4); + MEDIA_TYPE_VOICE(4), + MEDIA_TYPE_LIVE(5); private final int id; - private static Map map = new HashMap<>(); + private static final Map map = new HashMap<>(); static { for (MediaItemType type : MediaItemType.values()) { diff --git a/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java b/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java index 9ae87dbf..12505689 100644 --- a/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/ResponseBodyUtils.java @@ -1009,4 +1009,17 @@ public final class ResponseBodyUtils { return model; } + + public static StoryModel parseBroadcastItem(final JSONObject data) throws JSONException { + final StoryModel model = new StoryModel(data.getString("id"), + data.getString("cover_frame_url"), + data.getString("cover_frame_url"), + MediaItemType.MEDIA_TYPE_LIVE, + data.optLong("published_time", 0), + data.getJSONObject("user").getString("username"), + data.getJSONObject("user").getString("pk"), + false); + model.setVideoUrl(data.getString("dash_playback_url")); + return model; + } } diff --git a/app/src/main/java/awais/instagrabber/webservices/StoriesService.java b/app/src/main/java/awais/instagrabber/webservices/StoriesService.java index cb022372..4c079b04 100644 --- a/app/src/main/java/awais/instagrabber/webservices/StoriesService.java +++ b/app/src/main/java/awais/instagrabber/webservices/StoriesService.java @@ -121,12 +121,32 @@ public class StoriesService extends BaseService { final long timestamp = node.getLong("latest_reel_media"); final int mediaCount = node.getInt("media_count"); final boolean fullyRead = !node.isNull("seen") && node.getLong("seen") == timestamp; - final JSONObject itemJson = node.has("items") ? node.getJSONArray("items").getJSONObject(0) : null; + final JSONObject itemJson = node.has("items") ? node.getJSONArray("items").optJSONObject(0) : null; + final boolean isBestie = node.optBoolean("has_besties_media", false); StoryModel firstStoryModel = null; if (itemJson != null) { firstStoryModel = ResponseBodyUtils.parseStoryItem(itemJson, false, false, null); } - feedStoryModels.add(new FeedStoryModel(id, profileModel, fullyRead, timestamp, firstStoryModel, mediaCount)); + feedStoryModels.add(new FeedStoryModel(id, profileModel, fullyRead, timestamp, firstStoryModel, mediaCount, false, isBestie)); + } + final JSONArray broadcasts = new JSONObject(body).getJSONArray("broadcasts"); + for (int i = 0; i < broadcasts.length(); ++i) { + final JSONObject node = broadcasts.getJSONObject(i); + final JSONObject user = node.getJSONObject("broadcast_owner"); + final ProfileModel profileModel = new ProfileModel(false, false, false, + user.getString("pk"), + user.getString("username"), + null, null, null, + user.getString("profile_pic_url"), + null, 0, 0, 0, false, false, false, false, false); + final String id = node.getString("id"); + final long timestamp = node.getLong("published_time"); + final JSONObject itemJson = node.has("items") ? node.getJSONArray("items").getJSONObject(0) : null; + StoryModel firstStoryModel = null; + if (itemJson != null) { + firstStoryModel = ResponseBodyUtils.parseBroadcastItem(itemJson); + } + feedStoryModels.add(new FeedStoryModel(id, profileModel, false, timestamp, firstStoryModel, 1, true, false)); } callback.onSuccess(sort(feedStoryModels)); } catch (JSONException e) {