Build a Custom Accessible Audio Player
Creating custom audio players can be fun AND accessible! In this episode, Lindsey Kopacz teaches us how to build our own audio player in a way that’s usable by all of us.
Links & Resources
Click to toggle the visibility of the transcript
Captions provided by White Coat Captioning (https://whitecoatcaptioning.com/). Communication Access Realtime Translation (CART) is provided in order to facilitate communication accessibility and may not be a totally verbatim record of the proceedings.
JASON: Hello, everyone. Welcome to another episode of Learn with Jason. Today on the show, we have Lindsey Kopacz. Thank you so much for joining us today. Also, I just realized I never asked how to pronounce your last name.
LINDSEY: What you said was perfect. It depends on who you're asking. If you're asking someone Polish, they're going to say "co-patch." So you were pretty close. If Thomas is here, he's even correcting me. If you're American, I usually don't bother too much with corrections, but I say "co-patch" if I'm saying it like to whoever. If somebody gets really confused, I just say "co-paz."
JASON: Cool. So it's great to have you on the show. I feel like we've known each other for a while. We talk a lot. I've watched your work. It's super exciting we're going to get a chance to work together today. So for those of us who aren't familiar with your work, do you want to give us a little background on yourself?
JASON: Sure, yeah.
LINDSEY: Oh, yeah, and I just wrote a book and it released on Monday. That was really exhausting and exciting, too.
JASON: Yeah, and congrats on that.
JASON: Yeah, absolutely. And so, I mean, I feel like this is something that -- there's a lot of content around, but I feel like a lot of times the way that content is written, it feels like we're being told you're doing it wrong. Thank you very much for the bits, Coding Carter. So I feel like one of the things that's challenging as a developer, especially if you're new to all this, is that accessibility is something that feels, if you've only ever used a browser, like it's a layer removed from what we even know. We don't use screen readers. We don't use assistive tech. So it's hard to even understand how to know if we got it right. And that makes it even more challenging when a lot of times instead of learning HTML, we're learning an abstraction of HTML. You know, we started learning Boot Strap or used React or View or something that's writing our HTML for us in a lot of ways. So how do you -- (Laughter) Thank you, Xander, for the sub. I appreciate it.
LINDSEY: That was so cool.
JASON: So how do you, like, for someone who's brand new to the accessibility realm, how should they start? How do they get their feet wet with this?
LINDSEY: So I think -- oh, gosh.
JASON: That's a loaded question. I'm sorry. (Laughter)
JASON: Well, and so another thing that I think -- like something that was a big moment for me was when I realized that a lot of the accessibility stuff isn't new tech. It's like using the existing tech properly. Like if you write HTML, it's actually semantic as opposed to making everything a div. You're already so close on most of what you build. And that made it feel a lot less intimidating for me, when I realized like, oh, instead of doing a div class heading, I should just use an H1. Instead of using like a span with an on click, I should use a button or a link. You know, those types of things that I guess initially I was doing it because there's no styles attached to a span. That's easier for me to deal with. Realizing if I just reset the styles on a div with normalized CSS or whatever CSS reset you prefer, then everything works like that. And you can use the right tag and still style it however you want. So let's talk a little bit about what we're going to do today. Specifically, we're going to be digging into React. What did you want to build today?
LINDSEY: So, I want to build an audio player. I just did this for work like -- well, this year. The long year of 2020.
JASON: The longest decade. (Laughter)
LINDSEY: So I ended up building an audio player for our design system at work. I wanted to share some of the stuff I learned. I've already built -- I had already built an audio player before, but I think -- I don't remember how I did it, but there are so many things I did wrong. Even though I was thinking about accessibility, I'm just like, ugh, I would have changed that so much. So many things I would have done better. But yeah, so I wanted to build -- I wanted to talk a little bit more about the audio API and also because there's a lot of times when the reason why we build something custom is because we want to be able to style something that isn't really styleable -- however you say that. So like select list is an example of that. But that's another rant for another time because I don't actually like it when people create custom select lists. But the audio component was really fun for me because I got to see all of these APIs, all these media APIs that are like just built in. It made it -- once I learned about it, I was like, oh, this -- I can work with this. Also, this I didn't do so much at work, but something that I've noticed is a lot of times podcast players, we have other things that aren't on the native HTML audio element, like playback rate and skip 15 seconds or rewind 15 seconds. That's something that's common on podcast players. A lot of people like that functionality. I mean, I know I personally don't like listening to things at two speed, but I know plenty of people do.
JASON: I regularly meet people who have podcasts, and I'm like, why do you talk so slow? Because I listen to everything they do at 2X speed. Are you tired today? What's going on? (Laughter)
LINDSEY: Yeah, no, but I can't listen to things -- I think the fastest I listen to things is like 1.25, and that's if it's like a very slow audiobook or something. But for the most part, I'm like, woof, I can't listen to things that fast. But a lot of people do, and a lot of times -- I keep on saying audio components. The audio element does not have that. At least not that I know of. But there are playback rates in the API, which we can play around with. So yeah, so we can get started whenever.
JASON: Yeah, I'm ready. Let's do it. A quick shout out to the sponsors. If you go to lwj.dev/live, we have live captioning going right now. That's brought to us by Rachel at White Coat Captioning today. Thank you so much. And that is made possible thanks to the generous sponsorships of Netlify, Fauna, Sanity, and Auth0, who all kick in to make this show more accessible to more people. It means a lot to me. I hope that you get a lot of value out of it. Make sure you go check that out. Again, that's lwj.dev/live. Also, go make sure you follow Lindsey on Twitter. Let me drop a link to the captions as well so you can get those. Why on earth -- Slack always betrays me. I'm like, hey, be silent for two hours. It's like, no. So yeah, follow Lindsey on Twitter. This is the book. So go get a load of that. Is this the right page for it, by the way?
LINDSEY: Yeah, that is.
JASON: So here's a link to the book. Make sure you check that out. And we talked about accessibility insights from Microsoft. This is the right site, right? I just Googled it.
LINDSEY: Yeah, that's correct.
JASON: All right. So there are some tools to get us started. And with that, I think we are ready to rock and roll. So I have nothing set up. What should I do first?
LINDSEY: So I'm just going to use Create React app. If you want to just do like npx create react app, we can call it audio player if you want. Keep it simple.
JASON: Okay. Audio player. Let's call this accessible because this is going to be the repo name too. React audio player.
JASON: And we'll give that a second to do its thing.
LINDSEY: Yeah, so while it's doing that, if you want, let's actually open up the MDN -- like, let's search the MDN audio component or media -- actually, let's do the video and audio content from MDN. Let's scroll down a little bit. I think a lot of it's HTML focused. You can scroll down a little more.
JASON: Do we want these?
LINDSEY: Oh, let's do client web APIs maybe. I'm trying to remember where I was -- because I was like -- oh, wait. Actually, let's go search on MDN like media element as one word.
LINDSEY: It's all good.
JASON: Media element audio source node? Yeah, I think so. We'll just click on it. I think the thing I want to do is kind of show people -- actually, let me see if I can -- if I have it up. Oh, I have it up. I'll post it. Let me post it to the stream. Oh, that's really jarring when I'm looking at myself. Okay.
JASON: Yeah, the delay is very bizarre.
LINDSEY: Okay. I can put that in there. But basically, it's the HTML media element API is what I would look up.
JASON: HTML media element.
LINDSEY: Okay, never mind. I'm not going to try to find things on Twitch anymore. Let's scroll down and go through that and start looking at all these properties and events and methods we can use. So you can scroll down.
JASON: Oh, sorry. Yep, scrolling.
LINDSEY: It's all good. (Laughter) So if you look on the side bar, you can see things like current time, muted, duration, ended. So I like to think about these when I'm building an audio player. Okay, there's playback rate, there's obviously source and all these things. Then there's like play, pause. Then there are events like ended I think is one.
LINDSEY: Yeah, so those -- I like to take a look at those. When I was building this, I was like, okay, here's all these states. How can we build those states into React and how can we access those things? I'm guessing NPX is finished because I took a little longer with that.
JASON: Oh, yeah. Probably. Let's see. Yes, so that was accessible react. Let's open it up.
LINDSEY: Yes, cool. And we can just create that. So first thing's first.
JASON: Oh, hold on one second. I have to get init here or else it's going to ignore everything and make it hard to read. So let's open that one more time.
LINDSEY: Okay, no problem.
JASON: That's a little easier to read. How about that?
LINDSEY: Okay, cool. So, yes. The first thing I'm going to do just because I can't -- I don't like -- I like to start with a clean slate. I do appreciate that Create React app is a clean slate, but I want to go to the app.js and get rid of the logo and just kind of --
JASON: Should I just drop it all out?
LINDSEY: You can keep the -- yeah, I guess you can keep the app wrapper there. I just want to keep it empty. Yeah, we can -- hi, chat.
JASON: With Create React app, I think it's --
LINDSEY: You can just do yarn start. Whatever you prefer.
JASON: I tend to look whatever the lock file is. It always yells at me if I use the wrong one.
LINDSEY: Cool. So we can go ahead and get started with -- actually, let's go to the app CSS.
JASON: Oh, there's Tomas. We were talking about you earlier. Thanks for the sub. Sorry, what was I looking for? I definitely lost track.
LINDSEY: It's okay. We were like excited to see Tomas. Let's go to the app.css and kind of just delete all of the CSS in there. Sorry, not the -- the index CSS is fine, but the app.css.
JASON: Got it.
LINDSEY: Let's just delete all that just because.
JASON: All of it? So leave it empty.
LINDSEY: Leave it empty. We're not going to be using any of those classes anyway. Okay. So let's go back to -- actually, no, let's stay here. Can we go ahead and in the source directory, let's create like just a components folder. You know, standard stuff. Since we're only going to be doing one component, we can just do -- we don't have to separate it out into their own directories. We can just do audio player. We can do kabob case or whatever you want.
JASON: I have a preference here. I don't like capital letters in file names because we always run into an issue where someone forgets, then you change it and your git isn't case sensitive and everything breaks.
LINDSEY: Oh, okay. (Laughter) Actually, I think you don't need to do that with this one.
JASON: Oh, do they do the new thing?
LINDSEY: I think so. So let's just create an audio player component. We're going to do it like you said, a function component, or like you wrote. I always start out just with an empty -- like an empty audio tag. Let me find a -- actually, do you have any music that you want to listen to?
JASON: I have so many nonsense things that we can play here.
JASON: Let me take a look.
LINDSEY: I have one just in case you don't have one you want to use. But if you have one you want to use, you can feel free. Although, something I just thought of is I'm like, I'm not sure if people on Twitch will be able to hear.
JASON: They should be able to hear. Let me make sure that I'm actually piping audio through, and we will check. Should be going through this thing and that way. Are you going to play? Hello?
JASON: Are all of my sound effects like -- Dropbox, what are you doing?
LINDSEY: It's just being that way because you're streaming.
JASON: Yeah, this is exactly what I needed. Let's maybe go out of Dropbox. We had one over here. What are you doing?
LINDSEY: If you want, I have an external file too.
JASON: I'm now very concerned my computer has just decided it can't read mp3 anymore.
LINDSEY: Oh, no. Okay.
JASON: Hello? Hello? Testing. I don't know. I don't know what's going on. I have some in Cloudinary, though.
LINDSEY: Okay. If it's an external one, that's fine. I have an external mp3 that I got like for testing purposes. So we can always use that.
JASON: I think -- yeah, the only trick is that because this goes on YouTube, we have to be really careful about legal rights. So I tend to only use things that are either extremely royalty free or that I made myself. Let's see here. I'm just logging into Cloudinary off screen because you hackers always try to steal my secrets. Okay, almost there. And here is my Cloudinary. Let's dive in here.
You hackers. You dirty hackers.
JASON: That one works. Let's play this, though. Here's one.
Pew, pew, pew, pew.
JASON: Cool. That, by the way, is part of a new Easter egg on Jason.af. You can try to figure out where it comes from. But this can be our sound effect for today.
JASON: So we've got an audio element.
LINDSEY: Yes, so we have an audio element. Usually what I would do is pass the source as a prop, but because, you know, this was going to just sort of be static and for testing purposes, we can just hard code it for now. So we can just do SRC.
JASON: We could even do it like this.
LINDSEY: Oh, yeah.
JASON: Make it like a default. That way we could override it, but we won't.
LINDSEY: Okay, yeah. Actually, no, I like that better. Let's do that. So we'll have the source equals source.
JASON: Is this self-closing?
LINDSEY: It is self-closing, but, you know, I don't know if it'll yell at you or not. So the other thing I absolutely have to say is because this is just -- this isn't going to be the most complex transcription or anything, but we should have a required transcription prop when we're creating an audio component, even if it's being displayed somewhere else. I just -- I kind of have to say that.
JASON: So this is actually new to me because I don't know how this works. So what does this do?
LINDSEY: So this isn't anything that I would be putting into the API. Like, this isn't as much captions because we don't have a place to put captions that are, you know, in line with a video or anything. So I would just literally be putting this in a div or something.
JASON: Oh, okay.
LINDSEY: Below the audio. Just so, you know, I would make sure that we have -- it would just be a string that we render. So I would always put -- like pass in the transcription as -- what was it? What was the noise you made? I can't remember. Just pew, pew. (Laughter) Okay, I know this seems silly, but we should have a required transcription. The only reason I didn't do a required transcription for the component I created in the design system was because it was designed for a -- it was designed for a component that had transcriptions live. So it was kind of like moot. But we can put that underneath there.
JASON: Yeah, so that'll give us something. Then let's actually bring this in.
LINDSEY: Oh, right. That part.
JASON: Import audio player from components, audio player. Then we'll drop this down below. Okay. Then this comes in, and it looks like I did something wrong because our --
LINDSEY: Oh, you didn't put the controls in. That was my bad. I forgot to tell you. So in the audio, just type in controls, and you should see your controls.
JASON: Oh, look at that. Okay.
Pew, pew, pew, pewww.
JASON: Oh, good. I'm glad we're going to listen to this a thousand times today. Could you use the web API to generate a transcription?
LINDSEY: Technically, probably. I don't trust a lot of things for accuracy. Like, there's a whole thing about people saying like YouTube's auto captions are garbage and stuff. So I'm kind of, of the mind set, yes, but QA is important. So we have to make sure that -- I'm usually reluctant. When I do things that are auto generated, the QA usually takes more time than to write it myself.
LINDSEY: So -- because, like, there are times that -- like sometimes it is quicker, depending on how long it is. If it's a shorter thing, sometimes it's not that long to QA. If it's a longer thing, you're going to have to go through every single part of it, and that could be really, really daunting. Sometimes it's just easier to pay somebody to do it, if you can't do it yourself. But you know, I kind of warn people on relying on that stuff until it is absolutely certain how reliable it is. But you know, something I found is you think it's going to save you time, but a lot of times you get kind of caught up in QA because if you're neurotic like me, you're like --
JASON: There are some cool options out there that I've seen. Like Descript is kind of a cool approach to this. It does a speech to text transcription, but then it kind of puts it into a text editor in a way that you can edit. It's got some cool features. I haven't actually tried this yet, but a couple people I know have, and they said it's reasonably accurate. So this could be a good way to get most of the way there. I think at the end of the day, as much as -- like, if you don't have the money to pay for real transcription and you know you're not going to do it yourself, some transcript is better than no transcript, right?
JASON: Well, as long as it's not complete nonsense.
JASON: I mean, that's fair. There is a situation where you try to help and make it worse. I think that is something that we want to avoid. But okay. So now, where we are here is we have built this, and Nicky, to answer your question, this is how it goes.
Pew, pew, pew, pewwww.
JASON: Technically, we could ship this, and it works. But it's missing those features you mentioned, like playback rate and those sorts of things.
LINDSEY: Totally. So to start building this, I like to start out with pseudo code just because my brain works best with that. Let's go back to the audio player and, you know, wherever you want to write your pseudo code, we can do it. Let's start to think about all the things we need to do. So let's see. We need to create -- sorry?
JASON: Sorry, I'm just forgetting how computers work.
LINDSEY: Story of my life. (Laughter) Story of my life. I always forget how computers work. So first, let's think. I'm kind of just spitballing here. We need a play/pause toggle button.
JASON: Oh, right, right.
LINDSEY: So play/pause toggle button. We need to do a little current time versus duration, like the literal number values. So how much time has passed and how long the audio is. We need to create a scrubber. We need to create maybe -- so a playback rate pop-up maybe. Like a little pop-up button. And maybe we can do those buttons where you can rewind 15 seconds and fast forward 15 seconds, like a lot of podcasts have those. Let's see. Yeah, I like jump/rewind. And let's see.
LINDSEY: Oh, yeah, volume. Like a mute button, maybe. Hopefully we can get to all of these. But volume control, mute control. I don't want to say I put them on lower priority, but at the very minimum, at least you have your operating system. So that's what I'm probably going to deprioritize since you have your operating system, even though I like to have -- on my audio component, I do like to have those controls. Okay. So just as a kind of acceptance criteria -- sorry to get all PM-y --
JASON: No, that's fine. Someone needs to rein this show in.
LINDSEY: So we want to make sure all of these controls we can access with the keyboard. We want to make sure that all of them make sense on a screen reader. And we obviously want to make sure that we can use a mouse as well. So I try to think -- the reason why I kind of start with that is sometimes we're like, okay, we have to do all these toggles and stuff. We start jumping into the audio API without thinking about stuff we get for free. So when I first built this, I was like, okay, obviously we'll have buttons for those toggles. We'll have a -- I decided to go with an input range with the scrubbers and the volume controls.
LINDSEY: Then I try to think about like what things get labeled. So how do things get labeled? So I don't want to do this -- well, actually, let's be wild. Let's just do it. So do you know how to turn on your screen reader on -- (Laughter)
JASON: Uh, I've done this once, and I forget the key combination.
And that's just not something I'm willing to do. (Laughter)
JASON: Well-placed sound effect, Nicky. Well-placed.
LINDSEY: Do you have an older -- like do you have the F keys? Or do you have a newer --
JASON: I have F keys, yes.
LINDSEY: Okay. So it's command F5 to turn things on. I think. Well, if it's not, we can go to system preferences.
JASON: System preferences.
LINDSEY: Yeah, and then go to accessibility.
JASON: Oh, wait. My computer is just so cooked that it can't.
LINDSEY: Okay. There we go.
Voiceover on chrome, React App --
LINDSEY: Actually, let's pause that for a second.
JASON: Oh, God. Okay. Help.
LINDSEY: I was like, that was a bad judgment on my help. I should have told you how to interact first.
JASON: Yes, please. (Laughter)
LINDSEY: Before voiceover talks over me. So there's a lot of ways you can navigate with voiceover, but for me, for very basic things, the voiceover command is when you press control and option and the arrow keys. So when you press control and option at the same time and the arrow keys, that's how you can start moving through things. Then if you want to go deeper --
JASON: Oh, God. Oh, boy.
LINDSEY: Everything okay? Are you okay? Are you nervous?
JASON: I just really hope my -- I have system shortcuts that I really hope didn't stomp on this. So let's find out what happens. So when I turn on voiceover, I should be able to hit control and option and up and down?
LINDSEY: Yeah, so you might have to press the shift key in addition and press the down arrow to go inside the web page. But for the most part, yes, you can be using your down arrows or your right and left arrows.
Reload button. Interact with the title of button. Reload this page. Interact with the title of button.
LINDSEY: All right. Let's click on the wrapper of the page, really quick.
JASON: Oh, God.
You are currently on web content inside of a group. To enter the web area, press control, option, shift, down arrow. To exit this group, press control, option, shift, up arrow.
LINDSEY: Press control, option, shift, up arrow to get outside.
Out of React App web content.
LINDSEY: Okay. Now let's do control -- okay, sorry. I know where we are now. Let's do control, option, shift, down arrow again. Sorry. And let's do control, option, right arrow.
JASON: I think this is --
Play elapsed time, zero.
LINDSEY: Okay, so now, let's do control, option, shift, down arrow again.
Play, button. You are currently on a button. To click this button, press control, option, space.
LINDSEY: So now let's do control, option, right arrow.
Elapsed time, 0:02, group. You are currently in a group.
LINDSEY: Do that again.
0:02, total elapsed time. You are currently in a group.
LINDSEY: Okay. One more time.
Total elapsed time, 0:02, audio time scrubber 0:02, slider. You are currently on a slider. To start interacting with the slider, press control.
LINDSEY: Okay. You can turn it off now if you want.
JASON: That make sense. So basically, once we get into this, we move around and voiceover is going to tell us what is on the page and what we can do with it.
LINDSEY: Yeah, so the reason why I like to kind of go through with that is like, you know, I think sometimes it's really tempting to overanalyze. You're like, oh, my God, what are all these things I have to do? Sometimes I'm just like, turn on a screen reader and see what gets spouted to you. Whatever gets read to you, you want to try your best. Like, you're probably not going to be perfect, but you want to try your best to have those things get announced to you when you're on your controls. So the reason why I decided to make the scrubber input range is because the input range gets announced as a slider. So if you notice when we were on the scrubber, it said, you know, slider at the end. So that helped inform me like how I was going to use some things. Also with the play. I don't know if you noticed this, but play said button. So that also helps me. I'm like, okay, here are some elements I can just grab. Even though it might not be perfectly, exactly the same, we're going to be telling our user what to do to control that.
LINDSEY: Also, the input range and the buttons, they're focusable by default. You can interact with them. So all of the ways we expect to interact with them are very, very in alignment with how we expect to work with an audio component.
JASON: And we can kind of see this happen, if I don't use voiceover and just use my keyboard, I'm going to click on the heading and tap. Now I'm on the audio element. Then I tab again, and now I'm on the play button. Tab again, I'm on the slider. Tab again, I'm on volume.
LINDSEY: Yeah. So, when you're on the slider, if you go back to the slider when you're tabbing, if you press the left arrow and right arrow, you'll see that, that also controls how you interact with it, which is pretty much how an input range works.
JASON: And if I hit space, it interacts with that button. So I'm toggling mute on and off by pressing space.
LINDSEY: Yeah, exactly. So that's kind of, you know -- I try -- I think a lot of people have a tendency to get really in their head about things. I'm like, no, let's just think about, how does it work? So I just saw a question about how you tab in reverse. You press shift key in addition to tab.
JASON: So I'm just hitting shift, tab, and I'm going backwards.
LINDSEY: Oh, somebody already answered that.
JASON: Ximena always in with the help.
LINDSEY: But yeah, anyway. So, okay. Let's go ahead and what I like to do is do this step by step. So first I want to go -- let's go back to our code, and let's just get that play toggle working. So above the audio, let's just create a div wrapper, and we'll call that -- yeah, we'll create the button. For now, we're not going to do anything. We'll just press play. So obviously that's not going to do anything now.
JASON: Here's our button. It does nothing.
LINDSEY: It does nothing, which is what it should be doing because we didn't have any sort of event listeners. So let's go up to -- let's go up and import you state. There are going to be a lot of states we're going to be handling. Oh, my gosh. I don't know if you all can hear that. I'm at my in-law's house, and they have landlines.
JASON: It's like an archaeological dig over there.
LINDSEY: So let's call it is playing and set is playing just to kind of keep those -- keep that convention. And we'll start it off, yes, with false. You're ahead.
JASON: Oh, god. There we go. That's what I wanted.
LINDSEY: It's all good. So the first thing that -- oh, and also, since we're importing things, let's import use ref. It's going to be important that we can grab the audio component so we can use all of those handy APIs. So we'll do audio ref, and we can set it to null for now or whatever. Null or undefined. I'm not too picky about it. So before we toggle playing and is playing, let's actually go and set that ref on the audio player.
JASON: Audio player.
LINDSEY: Okay. So now let's go up to -- let me think. We're going to need to do a toggle plain function.
JASON: Just to keep our stuff together, I'm going to move this up above.
LINDSEY: Oh, yeah, go ahead.
JASON: That way we can kind of see it.
LINDSEY: Pseudo code belongs wherever you want it to go. So whatever is most readable to the audience. I'm going to -- let's create a function called toggle playing. Let's see. So the first thing I would do is let's just set the is playing to the opposite of what it was.
JASON: Okay. So set is playing is going to be --
LINDSEY: But we don't just want to do that. We want to make sure that we're actually using this state to decide if it's going to play or pause. Look at that.
JASON: So we can see our state is working even though this won't actually --
LINDSEY: Yeah, let's do that.
JASON: Hold on. Oh, we have to make it actually use the thing. There we go.
LINDSEY: That helps.
JASON: I'm getting there.
LINDSEY: For what it's worth, I didn't catch that either.
JASON: So hit play, then we can pause. Okay. And when we reload, it's false. It should play to start. Yes, perfect.
LINDSEY: So now what we'll do is right underneath, we'll create a -- we'll say is playing. So ref.current. Then we'll say ref.current -- actually, yeah. That's probably making things more readable.
JASON: Then we can do audio player.
LINDSEY: And we'll do the pause method. So just say pause.
JASON: Oh, just like that?
LINDSEY: Yep. Then the opposite is true if it's not. So let's just double check that is working now. Hopefully it's working.
Pew, pew, pew, pewwww.
LINDSEY: Oh, my god.
LINDSEY: So that's working. So we have our -- so it's doing what we need. It's toggling the label. It's also doing what we need it to do. It's playing and pausing. So, cool. Now, the next thing we can do, I like to keep the audio component next -- like, I don't hide the controls until after I get everything working. That's just a personal preference. Just because I'm a very visual person when it comes to debugging. I need to see it there if something is not working. Now the next thing we can do is create a duration -- like elapsed time and duration items.
LINDSEY: So before -- I'm sorry. After the play button, we can kind of create a span. This doesn't need to be, like, you know, overly semantic. What I'm going to do is -- because something I remembered it when we were listening to it on voiceover, it said elapsed time and then the time. So I'm going to -- like, we can use a visually hidden class afterwards, but I want to make sure the elapsed time is being read, and we can figure out later how we want to style that. Like I said, we're not worrying about styling right now. We're just making this work.
LINDSEY: Then we can create another span for the duration.
JASON: Is it like total duration? Do you remember what it said?
LINDSEY: Oh, god. I think it might have been total time. We can change that whenever we need to. So now what I want to do is I want us to create two states, one for media time and one for duration. So, yeah, we can just do media time and set media time. The default state for both of those we should have zero.
LINDSEY: Cool. All right. Then we can just replace those. We'll format the time later.
JASON: I was going to say, do we want to do this right now?
LINDSEY: It's not going to be that big of a deal because this is like a two-second clip, but if it was like --
JASON: Yeah, in this case it doesn't matter. If it was an hour long, it would be really hard to read.
LINDSEY: Honestly, I usually copy and paste somebody else's time formatter because I don't feel like figuring it out most of the time. But anyway, we have that. Now there's a couple other events that are happening that will help us figure out these things. So we have the loaded meta data event. That's like basically whenever the media gets floated. That's my most elementary, novice -- probably more in-depth people in the comments will yell at me. So we want to create an on-loaded meta data on the audio. Then we want to make sure that we're setting the duration on that. So just to repeat, on the audio element, we want to --
JASON: On the audio element, okay.
LINDSEY: Yes. We want to add an on-loaded meta data event, and we can name that whatever we want.
JASON: Does this work?
LINDSEY: So on-loaded meta data, we have to pass it as something we haven't created yet. What I like to do is on-loaded meta data, just to make sure that -- because something I learned earlier on is that sometimes there's like a split second that the audio hasn't loaded. So your duration will end up still being the same thing as the state. So I like to set the duration on an on-loaded meta data or -- is that what it's called? Let me double check that. Okay, yeah. So if you say loaded -- so it's loaded meta data.
JASON: Loaded meta data.
LINDSEY: Yeah, okay. All right. Let's look at it.
JASON: It probably is lower case "d." That seems like a good call.
LINDSEY: In that function that we just created, let's just set the -- okay. So we'll set duration. Then we'll get that audio ref current.duration. That's another --
JASON: Oh, it's just built right in.
LINDSEY: It's built into the audio API. Now if we refresh our app --
JASON: Let's see. It doesn't like that, and it could be because I typoed the event name.
LINDSEY: Oh, could be.
JASON: Let's take a look at the console. Set media time, that's fine. Leave all that alone.
LINDSEY: Let me double check.
JASON: So we never did call this, which means I got it wrong.
LINDSEY: That will help. So I do have it as onloaded meta data with a lower case "d."
JASON: Is that what I did? Onloaded meta data. Okay, so that should work. Hey, there it goes.
JASON: So I just needed to reload apparently.
LINDSEY: So that doesn't look cute. We're not going to worry about it too much right now. But that is the unrounded second time. Just as a note, this is not in milliseconds. If you want to use the date, you know, constructor or anything, you'll have multiply this by 1,000 because those would take milliseconds. These are the seconds. So now what we can do is there's another thing that we can put on our audio element. This one is not as annoying to remember. It's just called on time update. Basically what this will do is when we press play, this gets run. So what we want to do ideally on this one is we want to set the time -- set the media time to be equal to the current time. So I think current time is camel case.
JASON: We'll find out.
Pew, pew, pew, pewwww.
JASON: There it goes.
LINDSEY: Yeah, so this isn't pretty at all right now. Lots of formatting of the time needs to happen. But right now we have like a play button, and we have the elapsed time working and the duration working. So we're getting there, right. This is super cool.
JASON: And I think what's interesting about this is we're not doing magic here. We're just using built-in browser APIs, which is such a -- I feel like this was so not true, you know, 10, 15 years ago when I was first starting to write stuff and I had all these great ideas about -- you know, I was in a band at the time. I was like, I'm going to make these really immersive video players. I had to learn Flash. See this now, I'm thinking about all the pain and suffering that can be saved by learning these browser APIs versus the hand rolling of custom things in other platforms.
LINDSEY: Right, yeah. It's super neat. So it's cool, too, because you look at that and you're like, oh, it's updating. It's doing what you expect it to do. So now we can create the scrubber. The scrubber is going to be an input range.
JASON: I'll put type, range. And it needs a name, right? And the name should be?
LINDSEY: Yeah, so are you talking about a label or?
JASON: I'm not sure.
LINDSEY: Okay. So we can label it one of two ways. I personally prefer to have a physical label, so add an ID to the input and then matching it to a label. We can visually hide that, too. So we can just call this -- normally if this was something that would have multiple audio players on one page, which I don't know why you would, but --
JASON: Yeah, like if you had a podcast home page and you listed your episodes. There's a lot of reasons I could see that.
LINDSEY: You'd want that to be unique. You might want to use unique ID or something.
JASON: Well, and you should also -- probably, if you're loading a lot of episodes, you'd have information about those episodes. Maybe you'd have an episode ID or maybe other meta data.
LINDSEY: Yeah, totally.
JASON: In this case, we're hard coding. Screw it.
LINDSEY: Screw it, yeah. We can just call that the ID scrubber for now. But I just wanted to note to the watchers that I would usually make this a unique ID. And yes, we want to make sure we have a label with an HTML4. Yep, there we go. So we'll have that. So that value -- we'll add that value to be the media time.
JASON: Okay. You're going to have to help me on this. I've used the range only once before, so I don't know what the attributes are
LINDSEY: I'm sorry. It's a value attribute. The value gets set to the media time state.
JASON: Okay. Then do we need to set like a min and a max on it or something?
LINDSEY: Oh, yes, we do. So min will obviously be zero. And the max will be the duration.
LINDSEY: But I would actually put them as numbers to make it easier. Because the duration, I believe, is returned as an integer.
JASON: Okay. So there's our scrubber.
LINDSEY: So if we refresh that and press play --
Pew, pew, pew, pewwww.
JASON: Look at that beautiful automatic update. Are you seeing that? What I noticed is it went with pure integer time. How do we set it to respect the maybe tenth of a second or something?
LINDSEY: So this is something I would use with time formatters and stuff like that. I personally -- I'm trying to remember how I did this. I can't remember if I used fixed and put it as a number, but I think what's more important is there's actually an attribute called -- oh, what is it called?
JASON: What if I just hit it with a hammer and round these?
LINDSEY: If that's how you -- go ahead and do that. I don't think it's a big deal, especially for something --
Pew, pew, pew, pewwww.
JASON: Oh, I can't spell. It's not great. It'll work.
LINDSEY: Yes. So, let's see. So there's something called the -- this is something -- so right now with the range, it's communicating zero, one, or two. So we want to make sure -- like, that's fine, I guess, for now. If we have something that's hours long, we want it to communicate one hour and two minutes or something like that. And 30 seconds. So we can use this attribute called aria value now. I'm kind of -- we don't have to do this here, but I kind of just wanted to tell people about this attribute. If you're using a formatter, you can create a -- there you go. You can create something that says underneath the hood. So that way when you're on the scrubber and using a screen reader, it's not reading zero or one or two, it's saying zero second, one second, two seconds. So that's a really helpful thing. There's a lot of other aria attributes, like aria value now and aria value max and aria value this or that. We don't really need that on this because we have those all communicated with the min and max. These attributes are normally created for if you are custom making a slider. So because we're not custom making a slider, we only need that one to just communicate the value, if that makes sense.
JASON: For sure, yeah. As you're talking about that, I'm also trying to find somewhere in here I have a longer piece of music that's actually legal to use. I just can't remember where it ended up.
LINDSEY: Yeah, well, we can -- I'm more concerned -- like, if we have more time, we can work on formatting the text, but I'm more concerned about like making sure the scrubber works and stuff like that.
JASON: Yeah, so this one is 23 seconds. We can definitely make that work. So we'll swap that out once we get a little further along so we can test it with the jump forward, jump back stuff.
LINDSEY: Oh, right, yeah. The other thing is right now if we go to our app and try to use the scrubber, it's not going to work because we don't have a nonchange on it. So need to add an on change. I don't want to make it it on change. I normally would. You read my mind. In that, I want to get the target value. So an event. So we can just say -- and I also want to convert it to a number. That's important. It's going to return a string. You can either do parse flowed or wrap it around in a number. So, let's see.
JASON: Like that?
LINDSEY: Yeah, that should work. Then we can set the media time to that value, right.
JASON: Okay. But this won't effect the ref, right?
LINDSEY: No, it won't. You're on the right track. We have to do that.
JASON: I'm going to add this real quick. Here's our scrubber. I'm going to set an on change to be this so I don't forget.
LINDSEY: Why isn't it working? Because we didn't put it on.
JASON: We used to be able to move this. You can see as I'm moving this around, it's updating over here as well. So that's good. That's closer.
LINDSEY: Yes, but you'll see on the actual audio component itself, like on the audio element, it's not changing. So we have to make sure that we get that audio ref and set the current time to be that play head time. So we'll do the ref.current.current time. Then we'll set that to play head. I believe that should work. Yep.
JASON: There it goes.
LINDSEY: Cool. So we have --
JASON: And if we started in the middle --
JASON: It works. So we're actually setting this properly, which is exciting. Because this is so not helpful right now, I'm going to throw this longer track up here so we can use it.
LINDSEY: Okay, yeah. No, perfect.
JASON: And this will be a quick one to switch over, I think. There it is. Okay. Then I should be able to copy this instead, and we'll throw this -- actually, let's test it. So now, because we used our prop, we should see 23 seconds, good. (Music) Okay, so it's doing what we want. We can scrub around.
LINDSEY: Wait, I think something is going on with the state. Can we refresh that really quick?
JASON: Oh, that was an old one from before when we had the value set without an on change.
LINDSEY: Oh, okay. Yeah. I was like, huh, that's not reflecting. But yeah, if we see that now, we'll see it's updating, which is super cool. The thing that's really nice about this is all of this you can access with the keyboard. If you press your tab key now and press play, you should be able -- (Music playing) And now if you go to the scrubber, you should be able to rewind it and fast forward it. I mean, not a lot.
LINDSEY: So that's really cool. Just by using built-in elements, we're doing good things, right.
LINDSEY: I'm just like -- I'm just bopping along here.
JASON: Some day I'll get my act together. That's intended to be a theme song for the show. I just haven't had a chance to put together an opening scene.
LINDSEY: Oh, I'm honored. So let's go ahead and build a playback. So right now it could be -- sorry, I was reading the comments. So right now we could be using just some buttons or something, or we can have a dropdown. For the sake of simplicity, I'm going to just create a group of buttons, and we can worry about styling whenever.
JASON: Also just time checking. We have 22 minutes left.
LINDSEY: That's why I don't know if we want to make this some highly styled thing.
JASON: What if for now we just did a one or two, like even make it just a toggle?
LINDSEY: Yeah, I guess we could do that. I mean, what I did with this is I just created an array of like values and did a map through them. Just created a bunch of buttons for those. We can do 1, 1.2, a map. So speed. We can do just a button there, right. Then inside the button, we can obviously render speed and add the on click.
JASON: Okay. And the on click is going to be on change speed or whatever.
LINDSEY: Yeah, but what I found is -- because ideally, we want to pass in the rate, or the speed. So we want to do it that way and then pass in the speed there. So when we create the on change speed, we can pass in that rate. Sorry, I keep on saying rate. But speed.
JASON: And do we want to keep this if here?
LINDSEY: Yeah, we should do that. And we can set it as the default.
JASON: Okay. Thank you for the sub, Jordan. Okay. So we've got our speed. When we get this, we'll set speed to -- oh. We'll change this to new speed. We'll have to do the same thing down here, I think.
LINDSEY: Yeah. Well, won't those be scoped?
JASON: They will be scoped, but I've never not --
LINDSEY: Oh, okay. You don't want to confuse yourself. Makes sense.
JASON: Oh, no. Messed it up already. Initialization -- oh, yeah. Again, just forgot. I'm going to make this consistent. I made all these arrow functions, so we'll keep them all as arrow functions. There we go.
JASON: And I didn't do it right. Where are you, buttons? Is it actually returning anything? It's not returning anything. So these need to be parentheses instead of that. That will actually return these.
LINDSEY: Oh, then remove that thing there. And you should be good.
JASON: Okay. How many tries does it take?
LINDSEY: Hey, that's working better though.
JASON: Should have a unique key prop. Dang it. That's easier to read. Speed is assigned a value.
LINDSEY: So what we can do now is in that function we just created, we can set the playback rate. I would check the docs because I always camel case this wrong.
JASON: So the playback rate is going to be here.
LINDSEY: Yeah, so let's look for the playback rate. I think there's one more. That's the one we want. So the "b" is lower case.
JASON: Got it.
LINDSEY: I always mess this up. After this, let's do ref current playback rate, and that equals new speed.
JASON: So we don't need to track this in state, do we?
LINDSEY: Actually, I guess not. If you need to.
JASON: We're not really using it anywhere else. I guess if we were going to show this in the UI or something, it would make sense. In this instance, let's just not add indirection where we don't need it. So we can drop this out.
LINDSEY: No, sometimes I like to have it -- like if I'm making a dropdown or something, I like to have the actual value in the overall button that creates it. (Music playing)
JASON: It worked. Woo, okay. Coding Carter, I saw you posted the step attribute for the range. Yes, we absolutely could track -- like we could change the step to 0.1. I think, actually, I prefer in a scrubber for it to be whole seconds. I think that might just be me being --
Holy buckets, did that just work?
JASON: When the 5-hour energy kicks in. Yep, yep. But yeah, so okay. Now we've got a play/pause button. We've got a working scrubber. We have playback rate. One thing I noticed here is that if we start it here, it doesn't update here. So I think we need -- I feel like there are probably some events we can listen for.
LINDSEY: We should listen for those, absolutely. But just remember, these controls aren't going to exist anyway.
JASON: Fair enough, fair enough.
LINDSEY: But let's solve this. So let's go back to the audio. Actually, before we do that -- sorry, I'm jumping around. You can see my brain works at like 50 miles a minute. So let's go back to the API and look at what events happened. So what events happen on audio. What isn't happening here is we're not listening to an event on audio. We're listening to an event on the button. So we want to do on play, I think. And on pause. On play, we can set the state properly.
JASON: This is probably not the one to search for.
LINDSEY: Yeah, sorry. It's under the events. So if you go to the event side bar -- events is right there. Boom.
LINDSEY: Yeah, so we're going to use that. We'll do on play. Then on play, we can set is playing to true. Then we'll do the opposite on, on pause.
JASON: Okay. So we are going to add --
LINDSEY: We're going to add that to the audio element.
JASON: And we can drop these right in.
LINDSEY: Yeah, we don't need to create functions. I think it's set is playing.
JASON: Is playing. To true. And on pause, false.
LINDSEY: I believe that should fix our issue.
JASON: Okay. Let's give it a shot. Okay. Now, the only other thing I saw is I think when it ends --
LINDSEY: It stays, yeah. So that one is on ending.
JASON: Oh, it handled it.
LINDSEY: Did it?
JASON: Apparently pause fires at the end as well.
LINDSEY: Oh, interesting. Yeah, okay.
JASON: You may find yourself in an edge case with that one if you decide to build this audio player. So do some testing in other browsers, but I'm going to take that as a win given we've got 14 minutes left. So what else should we -- let's see. Let's look at our list.
LINDSEY: Oh, we have volume. We have the go 15 -- the rewind 15 and fast forward 15. The playback scrubber, playback rate. So let's do the jump and rewind.
JASON: Okay. And to do that, we can probably just add some buttons, right?
LINDSEY: Yeah, that's what I would do. So we could do like rewind 15 seconds.
JASON: I'm going to do five just because it's such a short --
LINDSEY: Fair. I'm just so used to my podcast players. It's just default to me. So these are probably going to be some of our simpler functions. All we have to do is set the media time to be 15 seconds -- or I'm sorry. See, I'm still stuck in podcast world. Five seconds. But we also have to do that for the audio ref as well.
JASON: Got it, okay. So I'm going to set these up.
LINDSEY: I like the on rewind, on fast forward.
JASON: Now we'll know what these are. I'm actually just going to copy/paste this function entirely. We'll go on rewind, and that's not going to get any event we need. And we're not going to use the playback rate. We're going to use the current time.
LINDSEY: Correct. And we also want to make sure we also set the media time state.
JASON: Okay. So we have the current time. We can say new time is going to be current plus -- or we're doing five. Rewind, minus five. Then we should probably do like a max of current minus five or zero so we don't try to rewind into negative time. Then we'll set the current time to the new time. And we also need to set --
LINDSEY: The state.
JASON: What did we call it, media time?
LINDSEY: Media time, yeah.
JASON: To new time. Okay. Now let's do the same thing but backwards. Fast forward, and this one will be plus, and we'll do a min. Okay. So what I'm doing here is I'm saying the current time plus five or whatever the duration is, whichever is lower so we don't go longer than the media clip. Okay. And that should work. We should be okay.
LINDSEY: Let's see.
JASON: Look at it go. Oh, my god. This is amazing.
LINDSEY: It's fun, yeah.
JASON: Here's where we find out if my math worked. This should, using five seconds, go to negative two seconds. But it didn't. I can math!
LINDSEY: Yay! Math is so much fun. I love math. Anyway.
JASON: Oh, my god. Someone clipped that. Okay. I'm happy. I'm really happy about this. This is great. So we now have the ability to play. All of these are buttons. So I can pop back and forth. I can change the playback speed, and we'll be able to see that a little more easily.
LINDSEY: I don't know how people can listen to their podcasts that fast.
JASON: Okay. So this needs a lot of work to look like an audio player, but functionality-wise, we've built this. As far as I can tell, this is completely accessible. We can use this whole thing. It's going to announce what it is. And right now, because we're not styling it, what it is, is what the screen reader will say. It will say elapsed time, 12, total duration, 23. We can change that to include seconds and stuff like that.
LINDSEY: Well, a lot of times we can use the formatting, right. So when we were using the screen reader for the audio component, we were noticing it was just saying current time, zero. That should make sense. I try to do it as closely as possible, but the time we have to do the seconds or whatever is when we're focused on the scrubber itself.
JASON: Right, right.
LINDSEY: Because then we want to make sure it's announced. So that's when aria value now comes in handy. Or, sorry, text -- what did I say it was?
JASON: Wasn't it aria value now? I have it up over here somewhere.
LINDSEY: It's somewhere. Maybe it's that last one. There, value now. Yeah, so it's the current range. No, wait, it's value text. That's what it is. The value text. So the value now is the integer value. The value text is what it would be read.
JASON: Oh, oh, okay. So basically, what you're saying is like if we're -- let's say -- let's look at this one. So we've got the aria value now would be media time.
LINDSEY: So that one -- so aria value now actually only goes on sliders. So anything with the role of sliders, so it would go on the input range. So the aria value now we don't need because we have our value. And that's what it sees. So aria value now is if we were creating a custom one and we were adding in that semantic value. But the value text is the one that we don't really get for free within input range.
JASON: Got it.
LINDSEY: So if we want it to be more specific and be like this is three seconds or 23 seconds or 1 hour and 23 seconds, you know, whatever. If you want to format your time however, that's really helpful there.
JASON: Right. So would we need to add context, like playback is at --
LINDSEY: No, because I think that's probably something I would add more to the playback area.
JASON: Okay. So more like this.
LINDSEY: Yeah, so that would be media time seconds, but obviously you would probably want to create a formatter if it was a really long one. For now, media time seconds is good. If you want to do -- because, what, we have not that much more time. But if you want, you can turn your screen reader back on and see what that announces as. We don't have to press the play button, so we're not overstimulated with noise.
Voiceover on chrome, react app, Google chrome, Jason presentations are window, watch learn with Jason live. In watch learn -- interact with the title.
LINDSEY: So you can just -- yeah, there you go.
Play, button, play.
JASON: How do I move?
LINDSEY: Control, option, shift, up arrow.
12 seconds, scrubber, slider. You are currently on a slider is inside of web content. To start interacting with the slider, press control, option -- visual studio code.
JASON: So if we change this --
You are currently on a slider inside of web content.
JASON: I'm going to use voiceover to make me feel better about myself.
12 seconds, Jason, you're doing a great job.
JASON: Thanks, voiceover.
You are currently on a slider. Voiceover off.
Holy buckets. Did that just work?
JASON: So how fun is that? This is super cool because I don't feel like anything we did was stepping outside of like my comfort zone as someone who can write HTML in React. This is the closest we got to doing something out of my day-to-day. It's just organizing it properly.
LINDSEY: Yeah. So the way I usually go about making accessible interactive components is I'm kind of like, well, what do we have for free? This is the only thing we didn't have for free. So the buttons, we get stuff for free. The input range, we get stuff for free. Some people complain that the input range is hard to style, but it's not really. It's just annoying to style it. Hard and annoying are two separate things.
JASON: Fair. (Laughter)
LINDSEY: So it's just a lot of code. I actually wrote a blog post about styling input range. If you go to my -- I wonder if it's -- yeah, it should be like the first thing there. Yeah, you can either go to the dev post -- it's the same blog post.
JASON: Let's give you the Google juice.
LINDSEY: So if you go down and see all of the CSS or whatever, there's a lot you can do to style it. I walk through like step by step how I styled it. You have to go through different studio elements and browser support. So ironically, IE is better support than Chrome with this.
JASON: Oh, wild.
LINDSEY: Yeah, I know. Who would have thought? So I go through a lot of things, like I use the linear gradient and stuff in that. I use -- I have a lot of examples. So that's what I say with Chrome. It's not the best because you don't have the lower portion, right. A lot of times we want to have some sort of progress indicator with a different color, which you can kind of -- I've sort of done, like hacked it with CSS variables and used the CSS variables to change the linear gradient In Chrome. I didn't do that in this blog post. It's probably something I should add. You can technically do it. I've done it before. I had to support Chrome for the feature I built, but you can technically style an input slider.
LINDSEY: But yeah, I was like, I don't think I'm going to go over this in this Twitch stream. People would probably by like, ahh.
JASON: And here come the corgis to send us off. Unfortunately, we're out of time. We have a list of resources we've gone over already. Are there any other things that people should go check out or any other places you want people to go and look? I'm going to do a forced drop of your book. Everyone go buy this book. It is a great resource. And at a bargain price. Like, you should have charged triple for this.
LINDSEY: I appreciate that. I wanted it to be, dare I say, accessible to people.
JASON: That's fair.
LINDSEY: But anyway, I would love to have people check out my blog. I try to write -- I haven't wrote as much in the year of 2020, but I try to write. And yeah, say hi to me on Twitter. Those are the ways to, you know, come find me and say hi.
JASON: Absolutely. So here's a direct link to Lindsey's site. With that, let's do one more shout out to the sponsors. We had White Coat Captioning. Thank you again to Rachel, who's helping us out today. That is made possible through sponsorships from Netlify, Fauna, Sanity, and Auth0, who all kick in to make this show more accessible to more people. Make sure you check out the schedule. We've got a lot of really fun stuff coming up. Next week on the show, we have Adam Barrett coming on to teach us about faster static site workflows with Nx. I thought it was only for mono repos. We're going to learn how that works, how it can make us faster. Then we're going to do something a little wild, a little risky. We're going to build a multiplayer soundboard, which means all of you, if you show up live, will be able to make noise on the stream while David and I are building things. It's going to be chaos. It's going to be a blast. Please tune in. The more of us there are, the more chaos there will be. That ends up being a lot of fun. With that, we're all set to go. Stay tuned, chat. We're going to go raid somebody. Lindsey, thank you so, so much for taking time out of your day to hang out with us.
LINDSEY: Yeah, thank you so much. This was fun.
JASON: I'm glad you had fun. I had a blast. I don't know if you could tell. Chat, I hope you had fun. And we will see you next time.
Closed captioning and more are made possible by our sponsors: