A Video Sharing App in 48 Hours
Last week, I was invited to an exclusive hackathon to build apps for musicians. The app team I was assigned to was tasked with building a video upload site for Bounce videos. Bounce is a style of music that originated in New Orleans. The app would be called BounceDotCom.com and there were plans to have Big Freedia, the Queen of Bounce, promote it. I knew the organizer could make things happen, so I jumped at the chance.
On the team was me, Brad Huber, and Doron Sherman, from Cloudinary. We had about 48 hours to make something happen. I showed up Monday evening, after the team had begun work, to figure out the plan and how I could help. There was a basic backend in Rails. I was going to come in early the next day and get to work on the frontend in JavaScript and React.
Now, people may know that I prefer ClojureScript myself over JavaScript. But I'm also a pragmatist. Although I think I could have done the job in ClojureScript in probably less time and code, I know that finding another ClojureScripter would be a difficult. It would tie the app to me. Any updates would depend on my schedule. Doing it in pure JavaScript would give much more flexibility, particularly for something where resources are tight and the future is unknown.
The next morning, I got to work setting up the React frontend so I could test it. I used create-react-app to get started. It comes with a dev setup so you can automatically reload your code as you save it. I'm a big fan of fast feedback. The save-and-reload workflow is not as good as you get in ClojureScript, but good enough for a small project like this. In ClojureScript, you don’t lose your current state when new code is reloaded, so there’s much less clicking.
My main focus at first was to get video uploads to work. I knew this would be the biggest challenge. Uploading files from multiple devices and posting to an API I was not familiar with was not something I wanted to mess around with on a short timeline. Plus, the app would be worthless without it. If people couldn't upload a video, the main concept of the site would not exist. Doron was a big help, providing the documentation when I needed it. Cloudinary offers many different solutions, including posting the video yourself or going through one of their widgets. For a 48-hour project, I chose a widget. There was no way I was going to trust that I could do it better in 48 hours.
When you’re working under sane conditions, spending a day to research your best option is well worth the investment. However, hackathon's are not sane. You want to quickly find something acceptable and move on. I found three different widgets that looked like they might work for our use case and our stack. In the end, the one that worked first was super easy. Just include this in the HTML:
Here’s what it looks like:
And on mobile:
That will fetch a script with everything you need. Here is how I show the widget.
window.cloudinary.openUploadWidget({ cloud_name: CLOUD_NAME, upload_preset: 'default', }, (err, result) => { });
You get the Cloud Name from your Cloudinary account. I had to create a preset in the dashboard that allowed for unsigned uploads so anyone could upload a video from their phone using only the frontend.
To display the videos, I tried cloudinary-react and it worked very easily.
import {Image, Video, Transformation, CloudinaryContext} from 'cloudinary-react'; <Video cloudName={CLOUD_NAME} publicId={VIDEO_ID} poster={`https://res.cloudinary.com/${CLOUD_NAME}/video/upload/${VIDEO_ID}.jpg`} ref={(v)=>this.video = ReactDOM.findDOMNode(v)} onPlay={()=>this.setState({playing:true})} onEnded={()=>this.setState({playing:false})} onPause={()=>this.setState({playing:false})} />
The component worked right out of the box, but we did have to fix some issues. It worked fine on desktop, but on my iPhone, the poster wasn't showing up. That's why I added the poster attribute manually in that code snippet. Problem solved. Luckily, Cloudinary is smart. If you ask for the .jpg file, it will give it to you and generate it if it needs to. If you ask for the .png, it does the same. It works better than you expect, because most services don't do this kind of transformation on the fly. But Cloudinary does, and it works the way you want it to work.
Notice that I set up a React ref for the video. I wanted to be able to stop and start the video in my scripts, so I needed a direct reference to the video element. The react-cloudinary components render out to regular HTML video elements.
How do I know?
I read the code. Yep, it's readable code. And when you're on the super tight deadline of a hackathon, you don't have time to read inefficient English text documentation. You go straight to the render() method. Code doesn't lie.
Another thing I learned from the code was that if the react-cloudinary components don’t understand a prop, they just pass them right down into the video element. So I could put onPlay, onEnded, and onPause callbacks right in there. A really nice touch.
Then I hit a snag.
The Cloudinary upload widget lets you upload videos and images. But there's no way to limit it to only videos. Well, not that I could find. If you said "only .mp4 files", it still lets you take a picture and try to upload it. Then it fails and you lose your picture. For our use case, that is a terrible user experience. People are having fun at a party, they take a really awesome picture they want to share with their friends, but instead the app drops the photo on the floor. The uploader works fine, but our app was never meant to host images. I could write a custom uploader, but I didn’t want to spend the time.
So what did I do?
I made an executive decision: We would support photos. This required a small backend change that I could not make, since I am clueless when it comes to Rails and Ruby. We needed to record whether it was an image or a video along with the media id. I made the changes to the frontend that I could, and allowed images to be displayed, and recorded an issue for the necessary backend changes in the backlog.
Here’s how you display an image from Cloudinary:
Super easy. The only thing I wish for here is that you could tell whether it was an image or a video from the id. I could query the API, but we wanted to minimize the number of requests to keep latency low. Plus, on mobile, each HTTP request is just another chance to fail.
So, by lunchtime, what did we have?
We could list videos (with inline playing and nice thumbnails) and upload new videos. We had a login system so we could identify users. Was it a nice app yet? No. Was it completely easy and straightforward? No, I can't say it was. But it was mostly forward progress. I mean, that was basically four hours of work to get video uploading with transcoding to multiple formats. Oh, and remember, it worked on desktop, iPhone, and Android with the same code. Not bad. And by “not bad”, I mean wow! A video app in a morning! I did not imagine this was possible before I met Cloudinary.
After lunch we started to put it on the open internet so we could have some kind of deployment pipeline. Until then, it was just me serving it from my laptop. We had some snags with that, too. For example, Heroku decided to upgrade its DNS to support SSL which did not allow us to add custom domains for a few hours. But in the end, we had everything hosted on Heroku.
At this point, it was 6 p.m. I had been adding a bunch of stuff to the backend backlog, since as I said, I don't know Rails. Lucky for us, Brad Huber, my teammate, knew plenty. I had to run but I would be back. I was hopeful to have all of my backend requests finished when I returned.
When I came back, it was on again. It was after 10 p.m. Some of my changes had been implemented, but not all. One of the things I requested was to be able to store arbitrary JSON data with users and with videos. In a hackathon, you just don't have time to mess around with designing a data model, and you certainly want to remove any reason to coordinate with the backend team. They have better things to do than add this field and that field to the model. It's much better to just let the frontend store whatever they want.
The break from coding had given me a new perspective on the app. I had been thinking about it mostly as a desktop web app. And our backend reflected that. It required users to register and login to upload a video. But after taking a break and seeing some issues logging in on some phones, I decided we needed to focus 100 percent on the main use case: the app would be demoed at a party the next night. People would want to pull out their phones, film some badass dancing, and upload it to share. They don't care about logging in. If they had to do that, they wouldn't have as much fun.
So how did we solve it?
We got rid of the login. You go to BounceDotCom.com, you click a button, record some video, and upload it. It shows up. You rock. That night, we recruited a couple of designers to draw some designs and implement it.
And then we passed the point of no return.
I hadn't eaten dinner. There was some food left I could scavenge from. And then Doron offered me a bubble tea. Great, I thought. It looked milky and those tapioca balls could sustain me. I started drinking it. And then I realized, too late, that it was coffee. I'm super sensitive to caffeine, especially that late at night. I doubted I would sleep that night.
And I didn't.
I stayed up all night coding on this app. There were several things I needed to do. We wanted a strong viral component, so I added Facebook sharing. To do that you need some Open Graph metadata in your HTML and some JavaScript for Like buttons. I hacked on that through the night. But Cloudinary made this really easy. Here's a snippet from the HTML template:
<meta property="og:image" content="{{&image}}" /> {{#video}} <meta property="og:video" content="{{&video}}" /> {{/video}} That {{&image}} and {{&video}} get replaced on the backend by this: if(pid && type === 'image') { image = `https://res.cloudinary.com/${CLOUD_NAME}/image/upload/${pid}.jpg`; } if(pid && type === 'video') { image = `https://res.cloudinary.com/${CLOUD_NAME}/video/upload/${pid}.jpg`; video = `https://res.cloudinary.com/${CLOUD_NAME}/video/upload/${pid}.mp4`; }
That is, we can generate image URLs and video URLs pretty easily for Facebook to use. Liking and sharing work pretty well. And it was thanks to Cloudinary's ease of use.
And then there was a surprise:
Travis Laurendine, the organizer, showed me that if you send a link over iMessage, it embedded the video right in there. Hello!! That was totally unexpected.
I crashed pretty hard around 10 a.m. I took a four-hour nap. When I woke up, I loaded the app to find it purple and beautiful, thanks to those designers. I fixed some CSS and added a play button. Everything was coming together. I worked on it a little that afternoon, but nothing so intense as before.
So what became of our demo?
In the end, the demo party never happened. It rained pretty hard and Jazz Fest kind of took over everything. But the app is there, still running and waiting.
With the main functionality of the app working, what’s next?
We have plans to migrate away from Heroku and onto a serverless cloud service. We don't really do much on the backend that couldn't be done cheaper and better on Google Cloud Platform or AWS. Using Lambda and Cloudinary, we basically have no overhead.
Low overhead is important for an app like this: if it doesn't take off, it costs next to nothing. But if it does, it will scale effortlessly. The other thing we might do is rewrite the uploading code. We're using the Cloudinary widget and we might want more control of the user experience. We'll want something customized where you click a button and it opens the camera, ready to record. However, I think that it will be complicated to get something working so well on all devices. It will have to wait. The Cloudinary widget works very well. It just does more than we need and those extra features could get confusing at a party.
I have to emphasize again that no one on the team had used Cloudinary before, except Doron, our contact at Cloudinary. Any app has engineering decisions that need to be made. Cloudinary’s employees, documentation, and code helped us stay on track. I am still surprised by how much we figured out and built in less than a day. The tools they give you, including the libraries, dashboard, and APIs, are where it really shines.
I look forward to hacking on this app in the future. And I’ll be dreaming up new ways to put Cloudinary to use.
Eric Normand is a long time functional programmer excited to see it entering the mainstream. He loves teaching and cooking. You can learn Functional Programming and Clojure from him at PurelyFunctional.tv and get inspired by The PurelyFunctional.tv Newsletter. If you visit him in New Orleans, you can meet his wife and daughter. He'll even make you some gumbo if you tell him you're coming. |