Giter Site home page Giter Site logo

eneim / toro Goto Github PK

View Code? Open in Web Editor NEW
1.4K 44.0 253.0 147.69 MB

Video list auto playback made simple, specially built for RecyclerView

License: Apache License 2.0

Java 77.91% Kotlin 5.13% HTML 16.96%
recyclerview video auto-playback exoplayer android

toro's Introduction

Toro

Video list auto playback made simple, specially built for RecyclerView

Download Android Arsenal Join the chat at https://gitter.im/eneim/Toro License

Buy Me a Coffee at ko-fi.com

Releases

Please grab latest demo APK and see release note here

Menu

Features

Core:

  • Auto start/pause Media playback on user interaction: scroll, open/close App.
  • Optional playback position save/restore (default: disabled).
    • If enabled, Toro will also save/restore them on Configuration change (orientation change, multi-window mode ...).
  • Customizable playback component: either MediaPlayer or ExoPlayer will work. Toro comes with default helper classes to support these 2.
  • Customizable player selector: custom the selection of the player to start, among many other players.
    • Which in turn support single/multiple players.
  • Powerful default implementations, enough for various use cases.

Plus alpha:

  • First class support for ExoPlayer 2.

Demo (Youtube Video)

Getting start, basic implementation

1. Update module build.gradle.

Latest version: Download

ext {
  latest_release = '3.6.2.2804' // TODO check above for latest version
}

dependencies {
  implementation "im.ene.toro3:toro:${latest_release}"
  implementation "im.ene.toro3:toro-ext-exoplayer:${latest_release}"  // to get ExoPlayer support
}

Using snapshot:

Update this to root's build.gradle

allprojects {
  repositories {
    google()
    jcenter()
    // Add url below to use snaphot
    maven { url 'https://oss.jfrog.org/artifactory/oss-snapshot-local' }
  }
  
  // TODO anything else
}

Application's build.gradle

ext {
  latest_snapshot = '3.7.0.2901-SNAPSHOT' // TODO check above for latest version
  // below: other dependencies' versions maybe
}

dependencies {
  implementation "im.ene.toro3:toro:${latest_snapshot}"
  implementation "im.ene.toro3:toro-ext-exoplayer:${latest_snapshot}"  // to get ExoPlayer support
}

2. Using Container in place of Video list/RecyclerView.

<im.ene.toro.widget.Container
  android:id="@+id/my_fancy_videos"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
/>

3. Implement ToroPlayer to ViewHolder that should be a Video player.

Sample ToroPlayer implementation (click to expand)

public class SimpleExoPlayerViewHolder extends RecyclerView.ViewHolder implements ToroPlayer {

  static final int LAYOUT_RES = R.layout.vh_exoplayer_basic;

  @Nullable ExoPlayerViewHelper helper;
  @Nullable private Uri mediaUri;

  @BindView(R.id.player) PlayerView playerView;

  SimpleExoPlayerViewHolder(View itemView) {
    super(itemView);
    ButterKnife.bind(this, itemView);
  }

  // called from Adapter to setup the media
  void bind(@NonNull RecyclerView.Adapter adapter, Uri item, List<Object> payloads) {
    if (item != null) {
      mediaUri = item;
    }
  }

  @NonNull @Override public View getPlayerView() {
    return playerView;
  }

  @NonNull @Override public PlaybackInfo getCurrentPlaybackInfo() {
    return helper != null ? helper.getLatestPlaybackInfo() : new PlaybackInfo();
  }

  @Override
  public void initialize(@NonNull Container container, @Nullable PlaybackInfo playbackInfo) {
    if (helper == null) {
      helper = new ExoPlayerViewHelper(this, mediaUri);
    }
    helper.initialize(container, playbackInfo);
  }

  @Override public void release() {
    if (helper != null) {
      helper.release();
      helper = null;
    }
  }

  @Override public void play() {
    if (helper != null) helper.play();
  }

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

  @Override public boolean isPlaying() {
    return helper != null && helper.isPlaying();
  }

  @Override public boolean wantsToPlay() {
    return ToroUtil.visibleAreaOffset(this, itemView.getParent()) >= 0.85;
  }

  @Override public int getPlayerOrder() {
    return getAdapterPosition();
  }
}


More advanced View holder implementations can be found in app, demo-xxx module.

4. Setup Adapter to use the ViewHolder above, and setup Container to use that Adapter.

That's all. Your Videos should be ready to play.

Proguard

If you need to enable proguard in your app, put below rules to your proguard-rules.pro

-keepclassmembernames class com.google.android.exoplayer2.ui.PlayerControlView {
  java.lang.Runnable hideAction;
  void hideAfterTimeout();
}

Advance topics

1. Enable playback position save/restore

By default, toro's Container will always start a playback from beginning.

The implementation is simple: create a class implementing CacheManager, then set it to the Container using Container#setCacheManager(CacheManager). Sample code can be found in TimelineAdapter.java. Note that here I implement the interface right into the Adapter for convenience. It can be done without Adapter. There is one thing worth noticing: a matching between playback order with its cached playback info should be unique.

2. Multiple simultaneous playback

Playing multiple Videos at once is considered bad practice. It is a heavy power consuming task and also unfriendly to hardware. In fact, each device has its own limitation of multiple decoding ability, so developer must be aware of what you are doing. I don't officially support this behaviour. Developer should own the video source to optimize the video for this purpose.

To be able to have more than one Video play at the same time, developer must use a custom PlayerSelector. This can also provide a powerful control to number of playback in a dynamic way.

Below is an example using GridLayoutManager and custom PlayerSelector to have a fun multiple playback.

layoutManager = new GridLayoutManager(getContext(), 2);
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
  @Override public int getSpanSize(int position) {
    return position % 3 == 2 ? 2 : 1;
  }
});

// A custom Selector to work with Grid span: even row will has 2 Videos while odd row has one Video.
// This selector will select all videos available for each row, which will make the number of Players varies.
PlayerSelector selector = new PlayerSelector() {
  @NonNull @Override public Collection<ToroPlayer> select(@NonNull View container,
      @NonNull List<ToroPlayer> items) {
    List<ToroPlayer> toSelect;
    int count = items.size();
    if (count < 1) {
      toSelect = Collections.emptyList();
    } else {
      int firstOrder = items.get(0).getPlayerOrder();
      int span = layoutManager.getSpanSizeLookup().getSpanSize(firstOrder);
      count = Math.min(count, layoutManager.getSpanCount() / span);
      toSelect = new ArrayList<>();
      for (int i = 0; i < count; i++) {
        toSelect.add(items.get(i));
      }
    }

    return toSelect;
  }

  @NonNull @Override public PlayerSelector reverse() {
    return this;
  }
};

container.setPlayerSelector(selector);

Behaviour:

3. Enable/Disable the auto-play on demand

To disable the auto play, simply use the PlayerSelector.NONE for the Container. To enable it again, just use a Selector that actually select the player. There is PlayerSelector.DEFAULT built-in.

4. Start playback with delay

It is expected that: when user scrolls so that the Player view is in playable state (maybe because it is fully visual), there should be a small delay before the Player starts playing. Toro supports this out of the box using PlayerDispatcher and via Container#setPlayerDispatcher(). Further more, the delay ca be flexibly configured by per-Player. The snippet below shows how to use PlayerDispatcher:

// ToroPlayer whose order is divisible by 3 will be delayed by 250 ms, others will be delayed by 1000 ms (1 second). 
container.setPlayerDispatcher(player -> player.getPlayerOrder() % 3 == 0 ? 250 : 1000);

5. Works with CoordinatorLayout

When using a Container in the following View hierarchy

<CoordinatorLayout>
  <AppBarLayout app:layout_behavior:AppBarLayout.Behavior.class>
    <CollapsingToolbarLayout>
    </CollapsingToolbarLayout>
  </AppBarLayout>
  <Container app:layout_behavior:ScrollingViewBehavior.class>
  </Container>
</CoordinatorLayout>

In the layout above, when User 'scroll' the UI by flinging the CollapsingToolbarLayout (not the Container), neither CoordinatorLayout will not tell Container about the 'scroll', nor Container will trigger a call to Container#onScrollStateChanged(int). But in practice, an interaction like this will make the Player be visible, and User expects a playback to starts, which may not without some update in your codebase.

To support this use case, Toro adds a delegation Container.Behavior that can be used manually to catch the behavior like above.

The usage looks like below:

// Add following snippet in Activity#onCreate or Fragment#onViewCreated
// Only when you use Container inside a CoordinatorLayout and depends on Behavior.
// 1. Request Container's LayoutParams
ViewGroup.LayoutParams params = container.getLayoutParams();
// 2. Only continue if it is of type CoordinatorLayout.LayoutParams
if (params != null && params instanceof CoordinatorLayout.LayoutParams) {
  // 3. Check if there is an already set CoordinatorLayout.Behavior. If not, just ignore everything. 
  CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params).getBehavior();
  if (behavior != null) {
    ((LayoutParams) params).setBehavior(new Container.Behavior(behavior,
        // 4. Container.Behavior requires a Container.BehaviorCallback to ask client what to do next. 
        new Container.BehaviorCallback() {
          @Override public void onFinishInteraction() {
            container.onScrollStateChanged(RecyclerView.SCROLL_STATE_IDLE);
          }
        }));
  }
}

Below is the behavior before and after we apply the code above:

Before After

Contribution

  • For development:

    • Fork the repo, clone it to your machine and execute ./gradlew clean build to start.
    • Latest Toro repository is developed using Android Studio 3.3.
  • Issue report and Pull Requests are welcome. Please follow issue format for quick response.

  • For Pull Requests, this project uses 2-space indent and no Hungarian naming convention.

Support

  • You can support the development of this library by buying me a cup of coffee (link below).

Buy Me a Coffee at ko-fi.com

Hall of Fame

Email to [email protected] with the description of your App using Toro to list it here.

License

Copyright 2017 eneim@Eneim Labs, [email protected]

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

toro's People

Contributors

archie94 avatar eneim avatar fossabot avatar paraplo avatar thiagoricieri avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

toro's Issues

Loop

Hello.
Is it possible to play a video in loop?

Play more than one video simultaneously while scrolling down and top.

More than one video play simultaneously while scrolling from down to top and top to down.

It is unable to cancel last requested video from top position and and previous top and current top both video play simultaneously.

This issue occur only when if request make to play and immediate scroll to another/next item/video in the list.

I have made changes in the library code to play video until 51% of top view/video is visible on the screen/viewport (In MyHolder class).

@Override public boolean wantsToPlay() {
        Rect childRect = new Rect();
        itemView.getGlobalVisibleRect(childRect, new Point());
        // wants to play if user could see at lease 0.51 of video
        return childRect.height() > mVideoView.getHeight() * 0.51
                && childRect.width() > mVideoView.getWidth() * 0.51;
    }
@Override
public float visibleAreaOffset() {
    Rect videoRect = getVideoRect();
    Rect parentRect = getRecyclerViewRect();
    if (parentRect != null /*&& !parentRect.contains(videoRect) && !parentRect.intersect(videoRect)*/) {
        return 0f;
    }
    float area = videoRect.height() * videoRect.width();
    float viewArea = getVideoView().getWidth() * getVideoView().getHeight();
    return viewArea <= 0f ? 1.f : area / viewArea;
}` 

Register timing will affect behavior

If we register RecyclerView to Toro before it lays out its child, things work normally. But if we setup later, there would be unexpected behavior (Video doesn't play)

Add the ability for developers to request audio focus

In my implementation I playback muted videos in a recycler view.
Unfortunately, if the user has another background music playing, the Toro video player will request focus automatically when the video is ready.

I would like the ability to not do this in order to provide auto playing muted videos without interrupting the user's music from another app.

How to disable auto play

How to disable auto play, but user can still play/pause manualy? or disable auto play when network is on GPRS, and enabl auto play when on WIFI or 4G?

Trying to Inflate in RecyclerView -> InflateException / ClassNotFoundException

Greetings! I don't believe this is an issue with your library so much as a configuration issue for which you may be able to offer valuable advice. I am trying to inflate a layout for a viewholder in my recyclerview which contains your Toro view. Unfortunately, I keep receiving both an InflateException and a ClassNotFoundException.

Any ideas on how to best configure my project?

gradle-wrapper.properties

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-all.zip

app/build.gradle

plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.1"

    defaultConfig {
        applicationId "twitcher.twitcherurlconnect"
        minSdkVersion 23
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
        multiDexEnabled true
    }

    dexOptions {
        javaMaxHeapSize "4g"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

ext {
    toro_latest_version = '1.2.0'
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:multidex:1.0.0'
    compile 'com.android.support:appcompat-v7:23.2.1'
    compile 'com.github.bumptech.glide:glide:3.6.1'
    compile 'com.android.support:design:23.2.1'
    compile 'com.yqritc:recyclerview-flexibledivider:1.4.0'
    compile "com.github.eneim:Toro:${toro_latest_version}"
    compile "com.github.eneim:Toro:1.2.0"

}

Device: Android: Nexus 5 API 23 x86 Emulator

Stack Tr
> E/AndroidRuntime: FATAL EXCEPTION: main
> Process: twitcher.twitcherurlconnect, PID: 29559
> android.view.InflateException: Binary XML file line #84: Binary XML file line #84: Error inflating class im.ene.lab.toro.player.widget.ToroVideoView
>     at android.view.LayoutInflater.inflate(LayoutInflater.java:539)
>     at android.view.LayoutInflater.inflate(LayoutInflater.java:423)
>     at twitcher.twitcherurlconnect.StatusesAdapter.onCreateViewHolder(StatusesAdapter.java:160)
>     at android.support.v7.widget.RecyclerView$Adapter.createViewHolder(RecyclerView.java:5464)
>     at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4689)
>     at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4599)
>     at android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:1988)
>     at android.support.v7.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1384)
>     at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1347)
>     at android.support.v7.widget.LinearLayoutManager.scrollBy(LinearLayoutManager.java:1174)
>     at android.support.v7.widget.LinearLayoutManager.scrollVerticallyBy(LinearLayoutManager.java:1031)
>     at android.support.v7.widget.RecyclerView$ViewFlinger.run(RecyclerView.java:4043)
>     at android.view.Choreographer$CallbackRecord.run(Choreographer.java:858)
>     at android.view.Choreographer.doCallbacks(Choreographer.java:670)
>     at android.view.Choreographer.doFrame(Choreographer.java:603)
>     at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:844)
>     at android.os.Handler.handleCallback(Handler.java:739)
>     at android.os.Handler.dispatchMessage(Handler.java:95)
>     at android.os.Looper.loop(Looper.java:148)
>     at android.app.ActivityThread.main(ActivityThread.java:5417)
>     at java.lang.reflect.Method.invoke(Native Method)
>     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
>     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
>  Caused by: android.view.InflateException: Binary XML file line #84: Error inflating class im.ene.lab.toro.player.widget.ToroVideoView
>     at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:776)
>     at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:704)
>     at android.view.LayoutInflater.rInflate(LayoutInflater.java:835)
>     at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:798)
>     at android.view.LayoutInflater.inflate(LayoutInflater.java:515)
>     at android.view.LayoutInflater.inflate(LayoutInflater.java:423) 
>     at twitcher.twitcherurlconnect.StatusesAdapter.onCreateViewHolder(StatusesAdapter.java:160) 
>     at android.support.v7.widget.RecyclerView$Adapter.createViewHolder(RecyclerView.java:5464) 
>     at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4689) 
>     at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4599) 
>     at android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:1988) 
>     at android.support.v7.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1384) 
>     at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1347) 
>     at android.support.v7.widget.LinearLayoutManager.scrollBy(LinearLayoutManager.java:1174) 
>     at android.support.v7.widget.LinearLayoutManager.scrollVerticallyBy(LinearLayoutManager.java:1031) 
>     at android.support.v7.widget.RecyclerView$ViewFlinger.run(RecyclerView.java:4043) 
>     at android.view.Choreographer$CallbackRecord.run(Choreographer.java:858) 
>     at android.view.Choreographer.doCallbacks(Choreographer.java:670) 
>     at android.view.Choreographer.doFrame(Choreographer.java:603) 
>     at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:844) 
>     at android.os.Handler.handleCallback(Handler.java:739) 
>     at android.os.Handler.dispatchMessage(Handler.java:95) 
>     at android.os.Looper.loop(Looper.java:148) 
>     at android.app.ActivityThread.main(ActivityThread.java:5417) 
>     at java.lang.reflect.Method.invoke(Native Method) 
>     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) 
>     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616) 
>  Caused by: java.lang.ClassNotFoundException: Didn't find class "im.ene.lab.toro.player.widget.ToroVideoView" on path: DexPathList[[zip file "/data/app/twitcher.twitcherurlconnect-1/base.apk"],nativeLibraryDirectories=[/data/app/twitcher.twitcherurlconnect-1/lib/x86, /vendor/lib, /system/lib]]
>     at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)
>     at java.lang.ClassLoader.loadClass(ClassLoader.java:511)
>     at java.lang.ClassLoader.loadClass(ClassLoader.java:469)
>     at android.view.LayoutInflater.createView(LayoutInflater.java:583)
>     at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:764)
>     at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:704) 
>     at android.view.LayoutInflater.rInflate(LayoutInflater.java:835) 
>     at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:798) 
>     at android.view.LayoutInflater.inflate(LayoutInflater.java:515) 
>     at android.view.LayoutInflater.inflate(LayoutInflater.java:423) 
>     at twitcher.twitcherurlconnect.StatusesAdapter.onCreateViewHolder(StatusesAdapter.java:160) 
>     at android.support.v7.widget.RecyclerView$Adapter.createViewHolder(RecyclerView.java:5464) 
>     at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4689) 
>     at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4599) 
>     at android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:1988) 
>     at android.support.v7.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1384) 
>     at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1347) 
>     at android.support.v7.widget.LinearLayoutManager.scrollBy(LinearLayoutManager.java:1174) 
>     at android.support.v7.widget.LinearLayoutManager.scrollVerticallyBy(LinearLayoutManager.java:1031) 
>     at android.support.v7.widget.RecyclerView$ViewFlinger.run(RecyclerView.java:4043) 
>     at android.view.Choreographer$CallbackRecord.run(Choreographer.java:858) 
>     at android.view.Choreographer.doCallbacks(Choreographer.java:670) 
>     at android.view.Choreographer.doFrame(Choreographer.java:603) 
>     at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:844) 
>     at android.os.Handler.handleCallback(Handler.java:739) 
>     at android.os.Handler.dispatchMessage(Handler.java:95) 
>     at android.os.Looper.loop(Looper.java:148) 
>     at android.app.ActivityThread.main(ActivityThread.java:5417) 
>     at java.lang.reflect.Method.invoke(Native Method) 
>     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) 
>     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616) 
>   Suppressed: java.lang.ClassNotFoundException: Didn't find class "im.ene.lab.toro.player.widget.ToroVideoView" on path: DexPathList[[dex file "/data/data/twitcher.twitcherurlconnect/files/instant-run/dex/slice-support-annotations-23.2.1_cc3ada849a687348dbc9a6080b8f35b918a42c4f-classes.dex", dex file "/data/data/twitcher.twitcherurlconnect/files/instant-run/dex/slice-slice_9-classes.dex", dex file "/data/data/twitcher.twitcherurlconnect/files/instant-run/dex/slice-slice_8-classes.dex", dex file "/data/data/twitcher.twitcherurlconnect/files/instant-run/dex/slice-slice_7-classes.dex", dex file "/data/data/twitcher.twitcherurlconnect/files/instant-run/dex/slice-slice_6-classes.dex", dex file "/data/data/twitcher.twitcherurlconnect/files/instant-run/dex/slice-slice_5-classes.dex", dex file "/data/data/twitcher.twitcherurlconnect/files/instant-run/dex/slice-slice_4-classes.dex", dex file "/data/data/twitcher.twitcherurlconnect
> 
> 

Any callbacks for knowing when an item in Recyclerview is active

Sorry if the answer is obvious as I just started testing this library today.

I am working with a fullscreen horizontal scrolling recyclerview where only 1 item is visible at any time. It can only be scrolled in one direction (from right to left). I would like to do few operations on items that have been scrolled past (i.e scrolled outside the visible area, like update db that it has been viewed, or delete from db, add analytics for the view etc..).

I was wondering if I could any of the TORO methods/helpers or if there are any callbacks which I can use to do the operations once the items are invisible after being visible.

Get the progress of video play back

Hi, @eneim

Thanks for this great Lib!

I used it on a Recyclerview and it works perfectly! However, I'm trying to implement a progress bar indicating the progress of the video playback.

In your README.md, Section 2.Toro in action, I notice that there is a digital progress note. But I can't find it when I run the project sample.

PS:
MyViewHolder extends ToroVideoViewHolder,
MyAdapter extends ToroAdapter<ToroAdapter.ViewHolder>

Thanks a lot!
progress

How to have a single custom view containing ToroVideoView and MediaController.

We have a scrollview which contains ToroVideoView and a MediaController (attached to ToroVideoView).
Now when we scroll, toroVideoView is scrolling where as the MediaController is not scrolling. We want the MediaController to scroll along with ToroVideoView, so that it gives the look and feel of one single view.

Playback inconsistencies

Hi,

Thanks for the great library. I have been trying to implement this on a recommendation from a different library (FastAdapter), but I have noticed a couple of issues. I can post these a separate issues if required.

  1. Playback off screen with a ViewPager - I have managed to combat that before with setUserVisibleHint from the Fragment. Although I am not sure how you would do this with your implementation yet
  2. Playback off screen whilst scrolling - If you scroll the play video off the screen, but then scroll very slightly whilst keeping it off the visible screen you can still hear the video playing even though it isn't visible. It seems like the visibility calculator isn't catching it for some reason. Either that or one of the next videos isn't switching from its Prepared state
  3. Looping - I notice you have this kind of setup in the Toro strategies. When will this become available? I want to loop the videos continuously, but know when they have stopped before they loop again. Or should this be done when onPlaybackStopped is called, but you don't have access to a MediaPlayer, so I'm not sure how to loop back around.

All of these points I can replicate in the supplied demo application. I am using very short content ≈8 seconds long, with sounds. Hence why I noticed some of these issues.

I am testing this on an Nexus 5 running Android 5.0 using Toro 1.1.0.

looping video

how to set looping on video using ExtVideoViewHolder or ExoVideoViewHolder?

Extend Toro to allow same functionality for Audio playing, pausing, looping as in Video

Currently I am achieving the above by

  1. In my ViewHolder view, I am placing a ToroVideoView with opacity 0.0 below an Custom View (the custom view is a custom Visualizer view). In future I may replace the custom view with ImageView for Album/song image/thumbnail
  2. With opacity 0.0 and Visibility.VISIBLE, ToroVideoView acts as placeholder for Toro's calculation of strategies when to play video, pause video etc..
  3. But for Audio, To achieve Autoplay Audio, pause audio, resume loop etc..I have something like the below.
@Override public boolean wantsToPlay() {
    // Default implementation
    boolean res = visibleAreaOffset() == 1.00;
    readytoplay1 = true;

    if (res && mMediaPlayer != null && readytoplay2) {
        try {
            mVisualizer.setEnabled(true);
            mMediaPlayer.start();
            handler.post(mUpdateVisualizer);
        } catch (Exception ex) {
            Crashlytics.logException(ex);
        }
    }

    return res;
}

@Override
public void onActivityPaused() {
    super.onActivityPaused();
    handler.removeCallbacks(mUpdateVisualizer);
    if (mMediaPlayer != null) {
        try {
            mMediaPlayer.pause();
        } catch (Exception ex) {
            Crashlytics.logException(ex);
        }
    }
}

public void onActivityDestroyed() {
    onDetachedFromParent();
}

@Override
public void onActivityResumed() {
    super.onActivityResumed();
    handler.post(mUpdateVisualizer);
    if (mMediaPlayer != null && readytoplay1 && readytoplay2) {
        try {
            mMediaPlayer.start();
        } catch (Exception ex) {
            Crashlytics.logException(ex);
        }
    }
}

@Override public void onAttachedToParent() {
    super.onAttachedToParent();
    if (mMediaPlayer == null) {
        try {
            mMediaPlayer = new MediaPlayer();
            mMediaPlayer.setDataSource(mItem.getUploadUrl());
            mMediaPlayer.prepareAsync();
            mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                @Override
                public void onPrepared(MediaPlayer mediaPlayer) {
                    setupVisualizerFxAndUI();
                    readytoplay2 = true;
                    if (readytoplay1 && readytoplay2) {
                        mVisualizer.setEnabled(true);
                        mMediaPlayer.start();
                        handler.post(mUpdateRunnable);
                        mMessagesAdapter.setToDelete(getAdapterPosition(), true);
                    }
                }
            });

            mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                @Override
                public void onCompletion(MediaPlayer mediaPlayer) {
                    mMediaPlayer.seekTo(0);
                    mMediaPlayer.start();
                }
            });

            mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
                @Override
                public boolean onError(MediaPlayer mediaPlayer, int i, int i1) {
                    return false;
                }
            });
        } catch (Exception ex) {
            Crashlytics.logException(ex);
        }
    }
}

@Override public void onDetachedFromParent() {
    super.onDetachedFromParent();
    if (handler != null)
        handler.removeCallbacks(mUpdateVisualizer);

    readytoplay1 = false;
    if (mMediaPlayer != null) {
        try {
            mMediaPlayer.pause();
            mMediaPlayer.release();
            mMediaPlayer = null;
            readytoplay2 = false;
        } catch (Exception ex) {
            Crashlytics.logException(ex);
        }
    }

    if (mVisualizer != null) {
        try {
            mVisualizer.release();
            mVisualizer = null;
        } catch (Exception ex) {
            Crashlytics.logException(ex);
        }
    }
}

So I believe something in Toro can be built for Audio play, based on what we already have for Video.
For ex- You may Create ToroAudioViewHolder, which instead of looking for ToroVideoView, looks for ToroAudioView. ToroAudioView can be a simple RelativeLayout inside which users can put ImageView for Album Image or custom view for Visulization. The idea is that the calculation should be done on the ToroAudioView RelativeLayout. After that how you manage the ToroVideoView, you manage the MediaPlayer same way to bind it with Toro's lifecycle and strategies.

In that case, we do not have to manage an instance of MediaPlayer in the viewholder and code becomes much cleaner

Service Leak Connection for youtube list

Logcat : - Activity demovideolist.com.demovideolist.MainActivity has leaked ServiceConnection com.google.android.youtube.player.internal.r$e@16ecdbfe that was originally bound here
android.app.ServiceConnectionLeaked: Activity demovideolist.com.demovideolist.MainActivity has leaked ServiceConnection com.google.android.youtube.player.internal.r$e@16ecdbfe that was originally bound here

Im gettting the above error while pressing the back button

Autoplay does not work when new Activity is started

  • the latest develop branch
  • Lg nexus 5x (android 7)

I tried your library and everything works just fine when I have a single activity, but when I open a new Activity with same logic (RecyclerView filled with video list) autoplay does not work for the first time.

Lets say we have two activities : FirstActivity(FA) and SecondActivity(SA). When I'm in FA and I open SA here is a sequence of activity lifecycle methods :

  1. SA -> onActivityStarted()
  2. FA -> onActivityStopped()

So the problem is that FA -> onActivityStopped() is called in the last place.

In my case, autoplay does not work for the first time as FA -> onActivityStopped() is called right after I do all the stuff to play first video in recycler view. onActivityStopped() method calls dispatchOnActivityInactive(activity) and that's how it stops video playback in SA.

Am I doing something wrong or there is a bug in your library?

Thanks.

Multiple concurrent playing videos

Apologies for a feature request in issues section. I can't seem to find the reddit I found the library in, if you have a link to that we could continue discussion there.

Would love to be able to have more than one video playing at a time by setting a max number in the strategy. Any prospect of implementing this ?

Lag in autoplay

Isnt there a lag in autoplay(SINGLE VIDEO SIMPLE LIST) when i scroll up and then scroll again down to the video. I think because prepare() is called again on that VideoView when i scroll back to it.

There is no case in which i just scroll back to the previous video and it plays without even a single millisecond delay. It always takes some time to prepare and resume the content.

TextureView in ToroVideoView destroys on opening another Activity

Suppose i open another Activity from ToroVideoView, in its onTouchEvent() function,then the TextureView gets destroyed and created again automatically. I get both onSurfaceTextureDestroyed and onSurfaceTextureAvailable callbacks one after other immediately.

How to provide max height to ToroVideoView

I am using com.github.eneim:Toro:1.2.0

In side recycler view video can be in different dimension. So I don't want to restrict the ToroVideoView to particular height. I just want to specify the maximum height of the ToroVideoView.

How to seek video in a recycler view to a specified position.

Using com.github.eneim:Toro:1.2.0

We have a recycler view which auto displays videos. On list item click we go to another detail activity which continues to display the same video. Now when we close the detail activity and come back to list view, we want that video to resume from a specified video position (basically the position of video where the detailed activity stopped).
We are trying to use Seekto(msec) in ToroVideoViewHolder callback methods. But it is playing the video from beginning.

Gradle configuration issue | Android studio

Hello Great Developer,

I am just trying to implement video player in Recycler view, but i am not able to configure the gradle of Toro.

Here is my code -



    // Choose the suitable one
// 1. Only use Toro
    compile "im.ene.lab:toro:2.0.0-SNAPSHOT"
// 2. Or use custom player widgets, backed by ExoPlayer
    compile "im.ene.lab:toro-player:2.0.0-SNAPSHOT"
// 3. Or even want to try some customized extensions for ease
    compile "im.ene.lab:toro-ext:2.0.0-SNAPSHOT"

    compile 'com.github.eneim:Toro:1.2.0'

With above Gradle, i am getting this error -


Error:Execution failed for task ':app:processDebugResources'.
> Error: more than one library with package name 'im.ene.lab.toro'

In Nested Fragment video is not playing

Hi @eneim As suggested by you, I tried implementing using ExoMedia and ToroPlayer both.
I have activity which have tabs. In the first tab, I have Fragment1 which have toro video implementation and it works fine. However in my fifth tab, I have nested fragment which have toro video implementation. It does not trigger playback.

Any idea how to resolve issue nested fragment issue ?

Thanks,
Pawan

How to disable autoplay in 2g and 3g

I want to disable autoplay feature in 2g and 3g and enable in 4g and wifi.
In 2g and 3g must play video on play click event and autoplay in other networks type.

UI hangs when network is slow (2G)

I am using com.github.eneim:Toro:1.2.0

In side the RecycleView list we have the videos for auto play.
When network is slow (like in 2G) entire RecycleView list UI hangs. I am not able to scroll the list.

I see lot of updates in repository. Is library has updated version.
Please let me know the best approach to handle it.

NullPointerException

FATAL EXCEPTION: ExoPlayerImplInternal:Handler
Process: forked.app.id.debug, PID: 24128
java.lang.NullPointerException
at com.google.android.exoplayer2.source.ExtractorMediaSource.onSourceInfoRefreshed(ExtractorMediaSource.java:179)
at com.google.android.exoplayer2.source.ExtractorMediaPeriod.maybeFinishPrepare(ExtractorMediaPeriod.java:408)
at com.google.android.exoplayer2.source.ExtractorMediaPeriod.access$000(ExtractorMediaPeriod.java:49)
at com.google.android.exoplayer2.source.ExtractorMediaPeriod$1.run(ExtractorMediaPeriod.java:120)
at android.os.Handler.handleCallback(Handler.java:733)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:136)
at android.os.HandlerThread.run(HandlerThread.java:61)
at com.google.android.exoplayer2.util.PriorityHandlerThread.run(PriorityHandlerThread.java:40)

Cannot Play video from url.. Though it played for the first time

Hi,
I have tried to implement your library in my app as it resolves one of the pain points of videos in Recycler view.

This implementation ran for the first time. But later on whenever i run the same app the videos won't play.
I get the following in my crash log:

05-03 19:15:12.898 31897-31897/com.playtm.android.lyfe_client D/ViewRootImpl: ViewPostImeInputStage processPointer 0
05-03 19:15:13.008 31897-31897/com.playtm.android.lyfe_client I/ToroViewHolder: VideoView Rect: 10 | Point(20, 2249) | Rect(20, 2404 - 1420, 3804)
05-03 19:15:13.008 31897-31897/com.playtm.android.lyfe_client E/ToroViewHolder: Parent    Rect: 10 | Point(0, 320) | Rect(0, 320 - 1440, 2560)
05-03 19:15:13.048 31897-31897/com.playtm.android.lyfe_client D/ViewRootImpl: ViewPostImeInputStage processPointer 1
05-03 19:15:13.078 31897-31897/com.playtm.android.lyfe_client V/MediaPlayer-JNI: native_setup
05-03 19:15:13.078 31897-31897/com.playtm.android.lyfe_client V/MediaPlayer: constructor
05-03 19:15:13.078 31897-31897/com.playtm.android.lyfe_client V/MediaPlayer: setListener
05-03 19:15:13.078 31897-31897/com.playtm.android.lyfe_client V/MediaPlayer-JNI: set_session_id(): 4202
05-03 19:15:13.078 31897-31897/com.playtm.android.lyfe_client V/MediaPlayer: MediaPlayer::setAudioSessionId(4202)
05-03 19:15:13.078 31897-31897/com.playtm.android.lyfe_client D/RingtoneManager: Can't get current user. return default user
05-03 19:15:13.078 31897-31897/com.playtm.android.lyfe_client W/MediaPlayer: Couldn't open file on client side; trying server side: java.io.FileNotFoundException: No content provider: http://res.cloudinary.com/playtm/video/upload/v1452364506/cuhod78iuoiab2kke1yw.3gp
05-03 19:15:13.078 31897-31897/com.playtm.android.lyfe_client D/MediaPlayer: setDataSource IOException | SecurityException happend : 
                                                                             java.io.FileNotFoundException: No content provider: http://res.cloudinary.com/playtm/video/upload/v1452364506/cuhod78iuoiab2kke1yw.3gp
                                                                                 at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:1141)
                                                                                 at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:991)
                                                                                 at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:914)
                                                                                 at android.media.MediaPlayer.setDataSource(MediaPlayer.java:1121)
                                                                                 at im.ene.lab.toro.widget.ToroVideoView.openVideo(ToroVideoView.java:635)
                                                                                 at im.ene.lab.toro.widget.ToroVideoView.access$2000(ToroVideoView.java:68)
                                                                                 at im.ene.lab.toro.widget.ToroVideoView$8.onSurfaceTextureAvailable(ToroVideoView.java:289)
                                                                                 at android.view.TextureView.getHardwareLayer(TextureView.java:368)
                                                                                 at android.view.View.updateDisplayListIfDirty(View.java:16441)
                                                                                 at android.view.View.draw(View.java:17238)
                                                                                 at android.view.ViewGroup.drawChild(ViewGroup.java:3921)
                                                                                 at android.view.ViewGroup.dispatchDraw(ViewGroup.java:3711)
                                                                                 at android.view.View.updateDisplayListIfDirty(View.java:16459)
                                                                                 at android.view.View.draw(View.java:17238)
                                                                                 at android.view.ViewGroup.drawChild(ViewGroup.java:3921)
                                                                                 at android.view.ViewGroup.dispatchDraw(ViewGroup.java:3711)
                                                                                 at android.view.View.updateDisplayListIfDirty(View.java:16459)
                                                                                 at android.view.View.draw(View.java:17238)
                                                                                 at android.view.ViewGroup.drawChild(ViewGroup.java:3921)
                                                                                 at android.view.ViewGroup.dispatchDraw(ViewGroup.java:3711)
                                                                                 at android.view.View.draw(View.java:17472)
                                                                                 at android.view.View.updateDisplayListIfDirty(View.java:16464)
                                                                                 at android.view.View.draw(View.java:17238)
                                                                                 at android.view.ViewGroup.drawChild(ViewGroup.java:3921)
                                                                                 at android.support.v7.widget.RecyclerView.drawChild(RecyclerView.java:3838)
                                                                                 at android.view.ViewGroup.dispatchDraw(ViewGroup.java:3711)
                                                                                 at android.view.View.draw(View.java:17472)
                                                                                 at android.support.v7.widget.RecyclerView.draw(RecyclerView.java:3308)
                                                                                 at android.view.View.updateDisplayListIfDirty(View.java:16464)
                                                                                 at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3905)
                                                                                 at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3885)
                                                                                 at android.view.View.updateDisplayListIfDirty(View.java:16424)
                                                                                 at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3905)
                                                                                 at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3885)
                                                                                 at android.view.View.updateDisplayListIfDirty(View.java:16424)
                                                                                 at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3905)
                                                                                 at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3885)
                                                                                 at android.view.View.updateDisplayListIfDirty(View.java:16424)
                                                                                 at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3905)
                                                                                 at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3885)
                                                                                 at android.view.View.updateDisplayListIfDirty(View.java:16424)
                                                                                 at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3905)
                                                                                 at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3885)
                                                                                 at android.view.View.updateDisplayListIfDirty(View.java:16424)
                                                                                 at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3905)
                                                                                 at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3885)
                                                                                 at android.view.View.updateDisplayListIfDirty(View.java:16424)
                                                                                 at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3905)
                                                                                 at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3885)
                                                                                 at android.view.View.updateDisplayListIfDirty(View.java:16424)
                                                                                 at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3905)
                                                                                 at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3885)
                                                                                 at android.view.View.updateDisplayListIfDirty(View.java:16424)
                                                                                 at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:3905)
                                                                                 at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3885)
                                                                                 at android.view.View.updateDisplayListIfDirty(View.java:16424)
                                                                                 at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:325)
                                                                                at android.view.ThreadedRenderer.updateRootDisplayList(Thr
05-03 19:15:13.078 31897-31897/com.playtm.android.lyfe_client V/MediaPlayer: setVideoSurfaceTexture
05-03 19:15:13.088 31897-31897/com.playtm.android.lyfe_client V/MediaPlayer-JNI: setAudioStreamType: 3
05-03 19:15:13.088 31897-31897/com.playtm.android.lyfe_client V/MediaPlayer: MediaPlayer::setAudioStreamType
05-03 19:15:13.088 31897-31897/com.playtm.android.lyfe_client W/MediaPlayer: setScreenOnWhilePlaying(true) is ineffective without a SurfaceHolder
05-03 19:15:13.088 31897-31897/com.playtm.android.lyfe_client V/MediaPlayer: setVideoSurfaceTexture
05-03 19:15:13.088 31897-31897/com.playtm.android.lyfe_client V/MediaPlayer: prepareAsync
05-03 19:15:13.088 31897-31909/com.playtm.android.lyfe_client D/MediaHTTPConnection: setReadTimeOut =  30000ms
05-03 19:15:13.088 31897-1426/com.playtm.android.lyfe_client D/MediaHTTPConnection: setReadTimeout with 30000ms

Tried on Lollipop and Marshmallow phones.
The videos load once and later don't work
I am using 1.2.0 library of TORO

Single video playback

Need to stop a video which doesn't want to play first, then look at the next one.

Now: check the next one first. If it wants to play, then save old video's state and pause playback --> this causes unexpected behavior in one-video list.

no content provider error

I am having below error when i try to load video in recyclerview. Some times it works some times my screen is empty.

W/MediaPlayer: Couldn't open file on client side; trying server side: java.io.FileNotFoundException: No content provider: /data/user/0/com.example.test/files/media/m_4083661.mp4
05-25 12:56:41.841 22929-22929/com.example.test W/MediaPlayer: setScreenOnWhilePlaying(true) is ineffective without a SurfaceHolder

Could you please check it and please let me know ?

Thanks. great library

v2 release date

Hey, realy nice library!!

Do you know when v2 will be released? Can't wait to try it out 👍

Gradle sync wont pass

Hi, today i cloned master branch and tried to start demo but gradle sync failed with this error:

Error:Could not find property 'bintray_user' on com.jfrog.bintray.gradle.BintrayExtension_Decorated@61eaae09.

have any suggestions how to resolve the issue ?

Unable to add Toro in android studio

Followed the configuration steps provided but gradle is showing Error:(31, 13) Failed to resolve: com.github.eneim:Toro:1.2.0.

Top level build.gradle is as below -

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
       maven { url "https://jitpack.io" }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

Module level build.gradle is as below -

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.3"

    defaultConfig {
        applicationId "in.appstute.myapplication"
        minSdkVersion 16
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}


repositories {
    maven { url "https://jitpack.io" }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.2.1'
    compile 'com.android.support:design:23.2.1'
    compile 'com.github.eneim:Toro:1.2.0'
}

Hope I will get quick response. Thanks in advance. :)

Crashlytics developer tool error

Log cat : - Error:Execution failed for task ':app:fabricGenerateResourcesDebug'.

Crashlytics Developer Tools error

Im getting the above error while building the project.

Please help me out

Support for autoplay and loop Audio

I guess adding support for Audio would be not very difficult by extending current framework. Is it something that can be accomodated in 1.2.0?

Bonus points for adding Visualization for Audio. 👍

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.