Skip to content

Commit ec5022d

Browse files
[feature] add mediaCapabilities hook (#30)
* add mediaCapabilities hook * update README.md with useMediaCapabilities hook * cleanup mediaCapabilities test * remove usage of useState and useEffect in media-capabilities * simplify things * add initialMediaCapabilities * add a better example in readme for useMediaCapabilities * better copy to align style of writing with writings from @mlampedx Co-authored-by: Addy Osmani <addyosmani@gmail.com>
1 parent d70fb69 commit ec5022d

File tree

4 files changed

+183
-0
lines changed

4 files changed

+183
-0
lines changed

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This is a suite of [React Hooks](https://reactjs.org/docs/hooks-overview.html) a
88
* [Data Saver preferences](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/saveData)
99
* [Device memory](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/deviceMemory)
1010
* [Logical CPU cores](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorConcurrentHardware/hardwareConcurrency)
11+
* [Media Capabilities API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Capabilities_API)
1112

1213
It can be used to add patterns for adaptive resource loading, data-fetching, code-splitting and capability toggling.
1314

@@ -28,6 +29,7 @@ import { useNetworkStatus } from 'react-adaptive-hooks/network';
2829
import { useSaveData } from 'react-adaptive-hooks/save-data';
2930
import { useHardwareConcurrency } from 'react-adaptive-hooks/hardware-concurrency';
3031
import { useMemoryStatus } from 'react-adaptive-hooks/memory';
32+
import { useMediaCapabilities } from 'react-adaptive-hooks/media-capabilities';
3133
```
3234

3335
and then use them in your components. Examples for each hook and utility can be found below:
@@ -156,6 +158,54 @@ const initialMemoryStatus = { deviceMemory: 4 };
156158
const { deviceMemory } = useMemoryStatus(initialMemoryStatus);
157159
```
158160

161+
### Media Capabilities
162+
163+
`useMediaCapabilities` utility for adapting based on the user's device media capabilities.
164+
165+
**Use case:** this hook can be used to check if we can play a certain content type. For example, Safari does not support WebM so we want to fallback to MP4 but if Safari at some point does support WebM it will automatically load WebM videos.
166+
167+
```js
168+
import React from 'react';
169+
170+
import { useMediaCapabilities } from 'react-adaptive-hooks/media-capabilities';
171+
172+
const webmMediaConfig = {
173+
type : 'file', // 'record', 'transmission', or 'media-source'
174+
video : {
175+
contentType : 'video/webm;codecs=vp8', // valid content type
176+
width : 800, // width of the video
177+
height : 600, // height of the video
178+
bitrate : 10000, // number of bits used to encode 1s of video
179+
framerate : 30 // number of frames making up that 1s.
180+
}
181+
};
182+
183+
const initialDecodingInfo = { showWarning: true }
184+
185+
const MyComponent = ({ videoSources }) => {
186+
const { mediaCapabilities } = useMediaCapabilities(webmMediaConfig, initialDecodingInfo);
187+
188+
return (
189+
<div>
190+
{
191+
mediaCapabilities.supported
192+
? <video src={videoSources.webm} controls>...</video>
193+
: <video src={videoSources.mp4} controls>...</video>
194+
}
195+
{
196+
mediaCapabilities.showWarning &&
197+
<div class="muted">
198+
Defaulted to mp4.
199+
Couldn't test webm support, either the media capabilities api is unavailable or no media configuration was given.
200+
</div>
201+
}
202+
</div>
203+
);
204+
};
205+
```
206+
207+
This hook accepts a [media configuration](https://developer.mozilla.org/en-US/docs/Web/API/MediaConfiguration) object argument and an optional `initialMediaCapabilities` object argument, which can be used to provide a `mediaCapabilities` state value when the user's browser does not support the relevant [Media Capabilities API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Capabilities_API) or no media configuration was given.
208+
159209
### Adaptive Code-loading & Code-splitting
160210

161211
#### Code-loading
@@ -258,6 +308,8 @@ export default App;
258308

259309
* [Device Memory API](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/deviceMemory) is available in [Chrome 63+, Opera 50+, Chrome for Android 76+, Opera for Android 46+](https://caniuse.com/#search=deviceMemory)
260310

311+
* [Media Capabilities API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Capabilities_API) is available in [Chrome 63+, Firefox 63+, Opera 55+, Chrome for Android 78+, Firefox for Android 68+](https://caniuse.com/#search=media%20capabilities)
312+
261313
## Demos
262314

263315
### Network

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export { useNetworkStatus } from './network';
22
export { useSaveData } from './save-data';
33
export { useMemoryStatus } from './memory';
44
export { useHardwareConcurrency } from './hardware-concurrency';
5+
export { useMediaCapabilities } from './media-capabilities'

media-capabilities/index.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2019 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
const useMediaCapabilities = (mediaConfig, initialMediaCapabilities = {}) => {
18+
let mediaCapabilities = {
19+
supported: typeof window !== 'undefined' && 'mediaCapabilities' in navigator,
20+
hasMediaConfig: !!mediaConfig
21+
}
22+
23+
mediaCapabilities = (mediaCapabilities.supported && mediaCapabilities.hasMediaConfig) ?
24+
navigator.mediaCapabilities.decodingInfo(mediaConfig) :
25+
{
26+
...mediaCapabilities,
27+
...initialMediaCapabilities
28+
}
29+
30+
return { mediaCapabilities };
31+
};
32+
33+
export {
34+
useMediaCapabilities
35+
};
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2019 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the 'License');
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an 'AS IS' BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { renderHook } from '@testing-library/react-hooks';
18+
19+
import { useMediaCapabilities } from './';
20+
21+
const mediaConfig = {
22+
type: 'file',
23+
audio: {
24+
contentType: 'audio/mp3',
25+
channels: 2,
26+
bitrate: 132700,
27+
samplerate: 5200
28+
}
29+
};
30+
31+
const mediaCapabilitiesMapper = {
32+
'audio/mp3': {
33+
powerEfficient: true,
34+
smooth: true,
35+
supported: true
36+
}
37+
}
38+
39+
describe('useMediaCapabilities', () => {
40+
test('should return supported flag on unsupported platforms', () => {
41+
const { result } = renderHook(() => useMediaCapabilities(mediaConfig));
42+
43+
expect(result.current.mediaCapabilities).toEqual({ hasMediaConfig: true, supported: false });
44+
});
45+
46+
test('should return supported and hasMediaConfig flags on unsupported platforms and no config given', () => {
47+
const { result } = renderHook(() => useMediaCapabilities());
48+
49+
expect(result.current.mediaCapabilities).toEqual({ hasMediaConfig: false, supported: false });
50+
});
51+
52+
test('should return initialMediaCapabilities for unsupported', () => {
53+
const initialMediaCapabilities = {
54+
supported: true,
55+
smooth: false,
56+
powerEfficient: true
57+
};
58+
59+
const { result } = renderHook(() => useMediaCapabilities(mediaConfig, initialMediaCapabilities));
60+
61+
expect(result.current.mediaCapabilities.supported).toBe(true);
62+
expect(result.current.mediaCapabilities.smooth).toEqual(false);
63+
expect(result.current.mediaCapabilities.powerEfficient).toEqual(true);
64+
});
65+
66+
test('should return hasMediaConfig flag when no config given', () => {
67+
Object.defineProperty(window.navigator, 'mediaCapabilities', {
68+
value: true,
69+
configurable: true,
70+
writable: true
71+
});
72+
73+
const { result } = renderHook(() => useMediaCapabilities());
74+
75+
expect(result.current.mediaCapabilities).toEqual({ hasMediaConfig: false, supported: true });
76+
});
77+
78+
test('should return MediaDecodingConfiguration for given media configuration', () => {
79+
Object.defineProperty(window.navigator, 'mediaCapabilities', {
80+
value: {
81+
decodingInfo: (mediaConfig) => mediaCapabilitiesMapper[mediaConfig.audio.contentType]
82+
},
83+
configurable: true,
84+
writable: true
85+
});
86+
87+
const { result } = renderHook(() => useMediaCapabilities(mediaConfig));
88+
89+
expect(result.current.mediaCapabilities).toEqual({
90+
powerEfficient: true,
91+
smooth: true,
92+
supported: true
93+
});
94+
});
95+
});

0 commit comments

Comments
 (0)