For the past few months, the frontend team at Bitmovin has been working hard to bring you our new web dashboard. The new dashboard is based on the unified Bitmovin API that we released earlier this year. It incorporates all of Bitmovin’s video infrastructure services and fits a wide array of end-to-end video workflows.
Since the beginning of our work on the portal, we had the mission to create a place where users can become familiar with the Bitmovin API and all of its services. The intention was to create a dashboard to easily monitor the status of encodings, configure the player and analytics domains, manage your account, and view usage statistics. Users can also create encodings and inspect configured encodings down to the very last API call straight from the dashboard.
In keeping with our tagline “Software to Solve Complex Video Problems”, we tried to create interfaces for the encoding, player, and analytics to be simple and intuitive, without compromising on features or flexibility we designed into our API.
Browsing our API spec, you might notice that there are hundreds of API calls, covering our products’ capabilities to the last bit. So, one could ask – what does our application architecture, testing, and deployment processes look like. And thus was born this blog post – for all of you familiar with React and Redux, and looking to know a bit more, as well as for the more experienced front end developers, interested in advanced techniques and processes we use.
We built the web portal as a single page application (SPA) using React and our bitmovin-javascript API client. There is no magic behind it – everything you see in the new portal is coming from the Bitmovin API using your credentials. Eating our own dog food to build the portal really helped us improve the API and let us better understand our customers and how they use our API clients to build applications.
Architecture
One of the strong arguments for using React is its “single purpose” approach. React does not try to solve all your Single Page Application problems, it simply solves the UI <-> Model binding issue and let’s you decide on where you want to go from there, without being opinionated or forcing its programming model on you.
It won’t take over your URL, it doesn’t care about data access or backend communication and it sure doesn’t care how you structure your application.
These properties make for a wonderful framework that can fit perfectly into your workflow. Unfortunately it makes it equally easy to shoot yourself in the foot. It’s also not helpful that there is a lot of conflicting documentation out there from people that are opinionated.
So we had to come up with our own opinions for React and Redux. We took the leap to full functional Redux style React development and had to firstly educate ourselves so we have a shared understanding of what exactly React/Redux means inside our team.
If there is no good consensus inside the team about the architecture and control flow inside your application, you’ll run into serious problems down the road, so here is what we ended up with.
Let me explain our architecture by following the data flow with a simple example.
Let’s say we have a Class Component
called Edit Encoding View
, which gets rendered to a view where you can add, delete, edit parts of an encoding job and start the displayed encoding by clicking the “Start Encoding” button.
Looking at the diagram above, we start at the “Class Components”. If a user clicks the “Start Encoding” button which is bound via the Redux mapDispatchToProps
function to the startEncoding
action. All of these “Action functions”, startEncoding
among them, get called as a result of some user interaction and dispatch “Events”.
These Actions contain our business logic for the application. They are responsible for handling a user or system interaction, trigger the appropriate API calls and emit the appropriate “Events” of what happened.
These “Events” then get processed by our State reducers and produce a new state object that correctly reflects the current state of the application. Redux then propagates these changes back to the UI accordingly.
The function body of startEncoding
is fairly easy; it dispatches an event called EditEncodingEncodingStart
, gets the Bitmovin API key from the application state and creates a Bitmovin API client object with that key.
The corresponding function of the Bitmovin API client gets called and returns a promise.
Finally, the promise resolving function dispatches an Event called EditEncodingEncodingStartSuccess
or, if the encoding could not be started, the event EditEncodingEncodingStartError
gets dispatched in the catch function.
export const startEncoding = (encodingId, bitmovinFactory = BitmovinFactory) =&gt; { return (dispatch, getState) =&gt; { dispatch(Events.startEncoding()); const bitmovin = bitmovinFactory(getState()); return bitmovin.encoding.encodings(encodingId).start() .then(result =&gt; { dispatch(Events.startEncodingSuccess()); }).catch(error =&gt; { dispatch(Events.startEncodingError()); }) } }Notable here is the optional function parameter
bitmovinFactory
which makes testing easy, as you just need to pass a mocked BitmovinFactory to prevent the function from using the real Bitmovin API client, while it will use the real API client in normal everyday use.
Following the diagram, the next component is the “State” and “Reducers”. The state is the application wide layer, which sets UI and application logic apart, most of what is not static and shown in the UI comes from the state. Often Redux gets overused and components end up having too many dispatching and mapping properties. This makes developers, not familiar with a component, have a hard time understanding what a component does. Simple logic, like a toggle button, can be done faster and more readable using stateful React components.
The state gets mutated by reducers, which are just switch statements. Reducers take a parameterstate
that gets initialized with the reducer’s default state, and a parameter, calledevent
, which has the value of any event dispatched by the action functions.
Before we continue, I’d like to note that, what we call an event, is usually an action in Redux language, and that our naming is different from common Redux actions. Common names would be something likeShowEncodingStarting
orAddTodo
,ShowAll
orSortDescending
. The reason for this is simply the amount of API interactions our dashboard handles. It becomes difficult to find an appropriate action name for every API interaction you make, so we decided to just express what we are doing in the action functions by dispatching events. In the case of ourEditEncodingView
, you find event names likeEditEncodingMuxingAdd
-Start, -Success or -Error and notEditEncodingShowMuxingAddingButtonLoading
(sounds weird, but this would be the proper action name) andEditEncodingAddMuxing
.
Thinking of Redux actions as events, rather than actions, enables us to have a simple naming pattern for events as you’re thinking “hey reducer, I did this” when doing something in the actions, rather than “hey reducer, do that”.
Returning back to our reducer for the “Start Encoding” example, the default state would look like this:{ startingEncoding: false, startedEncoding: false, hasError: false, error: '' }This default state gets returned the very first time when Redux initializes. After that, the state is mutated by 3 switch cases, each case copies the current state, changes some case specific values and returns the mutated state. The Event
EditEncodingEncodingStart
case, setsstartingEncoding
totrue
and returns the state,EditEncodingEncodingStartSuccess
setsstartingEncoding
tofalse
again, setsstartedEncoding
totrue
and returns the state, etc.
Until now, we covered how actions interact with the API, to dispatch events, and how the events mutate the state. Next, we’ll look at how mappers map the state to UI components.
For our “Start Encoding” example, the substatestartEncoding
of the global state gets mapped to the properties ofEditEncodingView
.
Mapping the state to properties would look like this:import {connect} from 'react-redux'; const mapStateToProps = (state) =&gt; { const {startEncoding} = state.encoding.encodings.editEncoding; return { state: { startEncoding } } }; export default connect(mapStateToProps)(EditEncodingView);The
mapDispatchToProps
, theEditEncodingView
class itself, other state parts, etc. have been omitted here to keep things simple and focus on mapping of thestartEncoding
state to theEditEncodingView
component.
We use mappers in two ways – one is where the mapping functions (state and dispatch) are in the UI component files itself, and one, where the functions are in a “mapper file”, which I’ll explain later.
Something that might be rather unusual here is that our state is built of “Sub Reducers”. If you are familiar with Redux, you might have experienced having one giant “Reducer Combiner” and, thus a very flat state. We decided very early on to use sub-reducers and a nested, cascading state.
Our “Encoding” “Reducer Combiner” (translates tostate.encoding.*
for the mapper functions) looks something like this:import {combineReducers} from 'redux'; import encodings from './encodings'; import manifests from './manifests'; ... export default combineReducers({ encodings, manifests, ... });The
encodings
function again combines Reducers likeeditEncoding
,encodingDetail
, etc.
Coming back to our start encoding example, here is a simplified logic to use the properties connected to the state:const {startingEncoding, startEncoding} = this.props.state.startEncoding; if (startingEncoding) { return &lt;Loading/&gt; } const {hasError, error} = this.props.state.startEncoding; if (hasError) { return &lt;ErrorMessage text={error}/&gt; } if (!startedEncoding) { return &lt;StartButton onClick={this.props.func.startEncoding}/&gt; } return &lt;Message text="Encoding started."/&gt;Thinking further about the separation of what data is displayed and where it comes from, enables us to reuse UI components and helps keeping things simple. In the case of our “Start encoding” example, we would extract the UI part of the
EditEncodingView
to a stateless functional component (SFC) calledStartResource
and have different mappers for starting an encoding, versus starting a manifest generation.
Another use of mappers would be having aResourcesList
UI component, and mappers for encodings, filters, inputs etc., which all use the same UI.
Example:ResourcesListsView
mounts mapperEncodingsListMapper
,InputsListMapper
andOutputsListMapper
, the mappers all use the sameResourcesList
component. If you were then to add sorting, you would only need to write the UI code once for all resources. It keeps things simple and follows the DRY (Do not Repeat Yourself) principle.Testing
Before using React and Redux, our old front end applications did not have any meaningful unit tests. Testing was done mostly by hand during development of a feature and then checked by QA. By strictly separating UI and application logic using Redux, we were able to test our business logic without having to resort to complicated UI testing frameworks like Selenium, or mucking around with headless browsers! From the start we wanted to have 100% coverage of both reducers and actions, and pull requests get instantly rejected if they push the coverage down instead of up!
So, how did we test our portal?
There are two kind of tests: Reducer tests and action tests.
Since Redux reducers are just functions that take a current state and an action object, reducer tests take the simple form of “given state A and event X, assert the resulting state is B”.
The action tests are a bit more complicated but follow the same principle: When called, they must dispatch the correct events. For the action tests we are using a mock of our Bitmovin API client, as we do not want the tests to actually interact with the API.
Reducers were tested by asserting different parts of a reducer’s state before and after events went through. Here it was important to test the reducer in smaller parts and not having one huge assertion which fails every time you add or change the reducers state object. Over-specifying reducer tests did cost us a lot of time in the beginning, because we had to fix them all the time when changes in the state were necessary. Since frontend development includes a lot of changing things we ran into this a lot!
Here is what testing the “Start Encoding” reducer could look like:describe('Start encoding reducer', () =&gt; { describe('when in default state', () =&gt; { it('should have startingEncoding set to false', () =&gt; { expect(defaultState.startingEncoding).toBe(false); }); describe('given a startEncoding event', () =&gt; { const event = Events.startEncoding(); it('should set startingEncoding to true', () =&gt; { expect(startEncodingReducer(defaultState, event).startingEncoding).toBe(true); }); }); }); });As you can see above we nest describe blocks and it specifications. The benefit of doing so is that the output of the jest test run is beautifully formatted making it easier to understand a test suite as if it was just one flat block of specifications. If test suites test larger states and reducers, separating the state into, wisely chosen, smaller blocks helps when it comes to changes as you mostly, only have to change one describe block if there was a change in the state’s structure.
Development Process
Now that we’ve covered how our portal works I’d like to describe our development process a bit.
After a feature becomes available in our API and we decide to add this feature to the dashboard, the Bitmovin javascript API client gets extended with this feature.
Once the feature becomes available in the javascript API client, there is a short discussion on how the UI should look and whether there are components we can reuse for this feature. During the implementation, continuous communication and review helps spot errors early and prevents situations where components get implemented twice, especially if the feature involves a lot of UI components.
All pushes on the feature branch of the dashboard repository trigger a CI process which executes the tests, pushes the coverage results to codecov.io, updates the static code analysis details on sonar and creates a production build of the dashboard. Builds on development, master branch and tags are pushed as Docker container, tagged with the output ofgit describe
, to a docker registry.
After implementing the feature and writing tests, a pull request gets issued by the individual contributor. To make reviewing easier we configured codecov.io to post a coverage report to every pull request. These reports show how much merging a pull request into the base branch would increase or decrease coverage.
Thinking further down the portal deployment process, the static files are served through a CDN, making sure initial load times are low. The origin is a HA Kubernetes deployment running Docker containers. The Docker containers execute a node server script using serve-static and express.js which makes it easy to set mime-type and directory specific cache-control headers. Our dashboard deployments follow the same multistage canary deployments as our service, a pattern developed by our infrastructure team and which got featured on the kubernets.io blog.Conclusion
Racing to keep up with the features our encoding, player and analytics product teams roll out, we will be adding integrations for DRM, Ads and an analytics dashboard to the Portal in the next few months.
At the time of writing, we’ve already disabled signups in our old system and moved all new signups over to dashboard.bitmovin.com. Please note that the new API is an independent system from our old API, which is why your credentials for app.bitmovin.com are not going to work in the new system. To get started with the new Bitmovin API, just head over to dashboard.bitmovin.com/signup and create a new account! If you are already a customer and want to migrate your existing subscription to the new Bitmovin API please get in touch with us! We created a simple tutorial that shows how you could create an encoding using the new portal.
We would love to hear your feedback on our new dashboard! Please tweet us at @bitmovin at let us know what you think!
We hope you enjoyed reading and this overview gives you an idea of how we built and tested our new portal. If you like building awesome things, come join us at bitmovin.com/jobs.Popular video technology guides and articles:
- Back to Basics: Guide to the HTML5 Video Tag
- What is a VoD Platform?A comprehensive guide to Video on Demand (VOD)
- Video Technology [2022]: Top 5 video technology trends
- HEVC vs VP9: Modern codecs comparison
- What is the AV1 Codec?
- Video Compression: Encoding Definition and Adaptive Bitrate
- What is adaptive bitrate streaming
- MP4 vs MKV: Battle of the Video Formats
- AVOD vs SVOD; the “fall” of SVOD and Rise of AVOD & TVOD (Video Tech Trends)
- MPEG-DASH (Dynamic Adaptive Streaming over HTTP)
- Container Formats: The 4 most common container formats and why they matter to you.
- Quality of Experience (QoE) in Video Technology [2022 Guide]