A native Titanium module that enables inline YouTube video playback without forcing the native OS player.
- Built with YouTubePlayerKit for iOS.
- Built with android-youtube-player for Android.
- β Inline playback (no forced fullscreen)
- β Configurable autoplay and loop
- β Mute/unmute control
- β Video quality control (Youtube is ignoring this for now)
- β Adjustable playback speed (0.25x - 2x)
- β Seek to any point in the video
- β Detailed state and metadata events
- β No native controls (Optional)
- β Caption support
- β
Fullscreen support (requires
showControls: true) - β Modern async API
- Titanium SDK 13.0.0+
- iOS 14.0+ / Android API 21+
Underlying players, pulled in by the module β you do not need to add them yourself:
| Platform | Library | Version |
|---|---|---|
| iOS | YouTubePlayerKit | 2.0.5+ (resolved via SPM at build time) |
| Android | android-youtube-player | 13.0.0 |
Download the latest version from the releases page.
# Copy the compiled module to:
{YOUR_PROJECT}/modules/iphone/ # iOS
{YOUR_PROJECT}/modules/android/ # AndroidAdd the module to your tiapp.xml:
<modules>
<module>ti.youtubeplayer</module>
</modules>The player runs inside a WKWebView. If your app tightens ATS, allow arbitrary
loads in web content in tiapp.xml:
<ios>
<plist>
<dict>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
</dict>
</plist>
</ios>const YouTubePlayer = require('ti.youtubeplayer');
const player = YouTubePlayer.createPlayerView({
videoId: 'dQw4w9WgXcQ',
autoplay: true,
loop: true,
showControls: false,
muted: true,
preferredQuality: YouTubePlayer.PLAYBACK_QUALITY_HIGH_RESOLUTION,
width: Ti.UI.FILL,
height: 300,
backgroundColor: '#000'
});
win.add(player);| Property | Type | Default | Description |
|---|---|---|---|
videoId |
String | required | YouTube video ID |
scalingMode |
Number | SCALING_ASPECT_FIT |
SCALING_ASPECT_FIT or SCALING_ASPECT_FILL |
loop |
Boolean | true |
Loop playback |
showControls |
Boolean | false |
Show YouTube controls |
muted |
Boolean | true |
Start muted |
showCaptions |
Boolean | false |
Show captions |
showFullscreenButton |
Boolean | false |
Show fullscreen button β requires showControls: true |
preferredQuality |
String | 'hd1080' |
Preferred quality ('small', 'medium', 'large', 'hd720', 'hd1080', 'highres') |
autoplay |
Boolean | true |
Start playback automatically |
startSeconds |
Number | 0 |
Position, in seconds, to start playback from |
keyboardControlsDisabled |
Boolean | true |
Disable keyboard shortcuts (iOS only) |
Note: All standard Ti.UI.View properties also work (width, height, top, left, backgroundColor, etc.)
- PLAYBACK_QUALITY_AUTO
- PLAYBACK_QUALITY_SMALL
- PLAYBACK_QUALITY_MEDIUM
- PLAYBACK_QUALITY_LARGE
- PLAYBACK_QUALITY_HD720
- PLAYBACK_QUALITY_HD1080
- PLAYBACK_QUALITY_HIGH_RESOLUTION
- SCALING_ASPECT_FILL
- SCALING_ASPECT_FIT
Starts video playback.
player.play();Safe to call immediately after creation. The player needs a moment to build its web view and load YouTube's iFrame API.
play(),pause(),mute()andunmute()issued before that is finished are recorded and applied as soon as the player becomes ready, so the common pattern below works without waiting for any event:const player = YouTubePlayer.createPlayerView({ videoId: id, autoplay: false }); container.add(player); player.play(); // aplicado assim que o player fica pronto
stop()cancels a pendingplay()rather than queueing.
Pauses video playback.
player.pause();Stops playback and rewinds to the beginning. A later play() starts the video again
from the start.
player.stop();
// ...
player.play(); // plays from 0:00Changed in 1.2.0. On iOS this used to map to the IFrame API's
stopVideo(), which unloads the video β aplay()afterwards did nothing, while the same code resumed on Android. Both platforms now pause and rewind. To drop the video entirely and free its memory, callrelease().
Mutes the video audio.
player.mute();Unmutes the video audio.
player.unmute();Returns whether the player is muted.
const muted = player.isMuted();
Ti.API.info('Muted: ' + muted);Returns: Boolean
Seeks to a specific point in the video.
// Jump to 30 seconds
player.seek(30);
// Jump to 1 minute 30 seconds
player.seek(90);Parameters:
seconds(Number): Position in seconds
Gets the total video duration.
player.getDuration(function(e) {
Ti.API.info('Duration: ' + e.duration + ' seconds');
});Callback returns:
duration(Number): Duration in seconds
Gets the current playback time.
player.getCurrentTime(function(e) {
Ti.API.info('Current time: ' + e.currentTime + ' seconds');
});Callback returns:
currentTime(Number): Current time in seconds
Sets the playback speed.
// Normal speed
player.setPlaybackRate(1.0);
// 1.5x faster
player.setPlaybackRate(1.5);
// 0.5x slower
player.setPlaybackRate(0.5);Parameters:
rate(Number): Speed (valid values:0.25,0.5,0.75,1.0,1.25,1.5,1.75,2.0)
Changes the current video.
player.changeVideo('dQw4w9WgXcQ');Parameters:
videoId(String): New video ID
Loads and plays a new video.
player.loadVideo({
videoId: 'dQw4w9WgXcQ',
startSeconds: 10 // Optional: start at 10 seconds
});Parameters:
videoId(String): Video IDstartSeconds(Number, optional): Start time in seconds
Loads a video without starting playback.
player.cueVideo({
videoId: 'dQw4w9WgXcQ',
startSeconds: 10 // Optional
});Parameters:
videoId(String): Video IDstartSeconds(Number, optional): Start time in seconds
Reloads the current player.
player.reload();Changes how the video is fitted inside the view at runtime.
player.setScalingMode(YouTubePlayer.SCALING_ASPECT_FILL);Parameters:
mode(Number):SCALING_ASPECT_FITorSCALING_ASPECT_FILL
Tears the player down and frees the underlying web view. Call it before removing the player from a window if you are not closing the window itself. The player cannot be reused afterwards.
player.release();Sets playback quality.
YouTube ignores this.
setPlaybackQualityhas been a no-op in the IFrame API since 2019 β quality is chosen by the player based on bandwidth and viewport size. The method is kept for API compatibility and stores your preference, but do not rely on it. On Android the underlying library exposes no quality API at all, andgetAvailableQualityLevels()always returns an empty array there.
player.setPlaybackQuality('hd1080');
// or
player.setPlaybackQuality(player.PLAYBACK_QUALITY_HD1080);Gets available qualities for the current video.
player.getAvailableQualityLevels(function(e) {
Ti.API.info('Available qualities: ' + JSON.stringify(e.levels));
});Callback returns:
levels(Array): List of available qualities
Fired when the overall player state changes.
player.addEventListener('playerStateChange', function(e) {
Ti.API.info('Player state: ' + e.playerState);
});Event properties:
playerState(String):'idle','ready','error'
Fired when the playback state changes.
player.addEventListener('playbackStateChange', function(e) {
Ti.API.info('State: ' + e.state);
Ti.API.info('Code: ' + e.code);
Ti.API.info('Is ready: ' + e.isFullyReady);
});Event properties:
state(String):'unstarted','ended','playing','paused','buffering','cued'code(Number):-1(unstarted),0(ended),1(playing),2(paused),3(buffering),5(cued)isFullyReady(Boolean): Indicates if the player is completely ready to receive commands
On iOS this is always
trueonceplaybackStateChangefires; the flag exists for Android, where the underlying player accepts commands slightly afterready.
Fired when playback quality changes.
player.addEventListener('playbackQualityChange', function(e) {
Ti.API.info('Quality: ' + e.quality);
});Fired when playback speed changes.
player.addEventListener('playbackRateChange', function(e) {
Ti.API.info('Playback rate: ' + e.rate);
});Event properties:
rate(Number): Current playback rate (0.25β2.0)
Fired when the player enters or leaves fullscreen.
Requires both
showFullscreenButton: trueandshowControls: true. The fullscreen button is rendered inside YouTube's own control bar (fsonly takes effect whencontrolsis on), so withshowControls: falseno button is drawn and this event never fires.
player.addEventListener('fullscreenChange', function(e) {
Ti.API.info('Fullscreen: ' + e.fullscreen);
});Event properties:
fullscreen(Boolean):truewhen entering fullscreen,falsewhen leaving
Fired when video metadata is loaded.
player.addEventListener('metadataReceived', function(e) {
Ti.API.info('Title: ' + e.title);
Ti.API.info('Author: ' + e.author);
Ti.API.info('Video ID: ' + e.videoId);
});Event properties:
title(String): Video titleauthor(String): Channel namevideoId(String): Video ID
Fired when mute state changes.
player.addEventListener('muteChanged', function(e) {
Ti.API.info('Muted: ' + e.muted);
});Event properties:
muted(Boolean):trueif muted,falseif unmuted
Fired when an error occurs.
player.addEventListener('error', function(e) {
Ti.API.error('Error: ' + e.message);
Ti.API.error('Code: ' + e.code);
});Event properties:
message(String): Error messagecode(Number): Error codetype(String): Error type
YouTube IFrame API codes:
| Code | Type | Message |
|---|---|---|
| 2 | invalid_parameter |
Invalid video ID |
| 5 | html5_error |
HTML5 player error |
| 100 | not_found |
Video not found, private, or age-restricted |
| 101 | embedding_disabled |
Owner doesn't allow embedding |
| 150 | embedding_disabled |
Same as 101 |
| 153 | missing_referer |
Missing HTTP Referer header or API Client identification |
Transport/setup failures reported by the module itself (negative codes):
| Code | Type | Message | Platform |
|---|---|---|---|
| -1 | iframe_api_failed |
The YouTube iFrame API failed to load | iOS |
| -2 | web_process_terminated |
The web content process was terminated | iOS |
| -3 | setup_failed |
Player setup failed | iOS |
| -4 | navigation_failed |
WebView navigation failed | iOS |
| -99 | unknown |
Unrecognised player error | iOS, Android |
The API is the same on both platforms, but the underlying players are not. These are the behaviours that genuinely differ:
| Topic | iOS | Android |
|---|---|---|
setPlaybackQuality() |
Forwarded to the player, which YouTube ignores. | No-op: the library exposes no quality API. |
getAvailableQualityLevels() |
Returns what the player reports. | Always returns an empty array. |
preferredQuality |
Applied as a hint, ignored by YouTube. | Stored only. |
keyboardControlsDisabled |
Supported. | Not applicable. |
| Recycled rows | Reconfiguring a recycled ListView/TableView row applies the new properties in place, rebuilding the player only when an iFrame-level option (autoplay, loop, showControls, showCaptions, showFullscreenButton, startSeconds) actually changed. |
Same properties, but the row is rebuilt from scratch. |
error codes |
Full range, including the negative transport codes below. | YouTube codes, plus -99 for unrecognised errors. |
| Playback rate | 0.25β2.0 in eight steps. |
Four steps: 0.25, 0.5, 1.0, 1.5, 2.0; other values fall back to 1.0. |
Quality control is a YouTube limitation, not a module one: setPlaybackQuality has
been a no-op in the IFrame API since 2019, and the player picks quality from bandwidth
and viewport size.
const YouTubePlayer = require('ti.youtubeplayer');
const win = Ti.UI.createWindow({
backgroundColor: '#fff'
});
const player = YouTubePlayer.createPlayerView({
videoId: 'dQw4w9WgXcQ',
autoplay: true,
loop: true,
muted: true,
width: Ti.UI.FILL,
height: 300,
top: 0
});
const muteButton = Ti.UI.createButton({
title: 'π',
width: 50,
height: 50,
right: 10,
top: 10,
backgroundColor: '#000',
opacity: 0.7,
borderRadius: 25
});
muteButton.addEventListener('click', function() {
if (player.isMuted()) {
player.unmute();
} else {
player.mute();
}
});
player.addEventListener('muteChanged', function(e) {
muteButton.title = e.muted ? 'π' : 'π';
});
win.add(player);
win.add(muteButton);
win.open();const YouTubePlayer = require('ti.youtubeplayer');
const videos = ['dQw4w9WgXcQ', 'kJQP7kiw5Fk', 'L_jWHffIx5E'];
let currentIndex = 0;
const player = YouTubePlayer.createPlayerView({
videoId: videos[0],
autoplay: true,
loop: false,
width: Ti.UI.FILL,
height: 300
});
player.addEventListener('playbackStateChange', function(e) {
if (e.state === 'ended') {
currentIndex = (currentIndex + 1) % videos.length;
player.changeVideo(videos[currentIndex]);
}
});
win.add(player);const YouTubePlayer = require('ti.youtubeplayer');
const player = YouTubePlayer.createPlayerView({
videoId: 'dQw4w9WgXcQ',
autoplay: false,
showControls: false,
width: Ti.UI.FILL,
height: 300
});
const controlsView = Ti.UI.createView({
height: 60,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.7)'
});
const playButton = Ti.UI.createButton({
title: 'βΆοΈ',
left: 10,
width: 50
});
const pauseButton = Ti.UI.createButton({
title: 'βΈ',
left: 70,
width: 50
});
const progressLabel = Ti.UI.createLabel({
text: '0:00 / 0:00',
right: 10,
color: '#fff'
});
playButton.addEventListener('click', function() {
player.play();
});
pauseButton.addEventListener('click', function() {
player.pause();
});
// Update progress every second
setInterval(function() {
player.getCurrentTime(function(e) {
player.getDuration(function(d) {
const current = Math.floor(e.currentTime);
const total = Math.floor(d.duration);
progressLabel.text = formatTime(current) + ' / ' + formatTime(total);
});
});
}, 1000);
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return mins + ':' + (secs < 10 ? '0' : '') + secs;
}
controlsView.add(playButton);
controlsView.add(pauseButton);
controlsView.add(progressLabel);
win.add(player);
win.add(controlsView);const YouTubePlayer = require('ti.youtubeplayer');
const player = YouTubePlayer.createPlayerView({
videoId: 'dQw4w9WgXcQ',
autoplay: true,
muted: true,
width: Ti.UI.FILL,
height: 300
});
player.addEventListener('playbackStateChange', function(e) {
// Wait for player to be fully ready before executing commands
if (e.state === 'playing') {
Ti.API.info('Player is ready! Safe to call commands now.');
// Now it's safe to unmute
player.unmute();
// Or change speed
player.setPlaybackRate(1.5);
}
});
win.add(player);If you want to persist the mute state between app sessions:
const YouTubePlayer = require('ti.youtubeplayer');
// Load saved mute state
const savedMuteState = Ti.App.Properties.getBool('my_youtube_muted', true);
const player = YouTubePlayer.createPlayerView({
videoId: 'dQw4w9WgXcQ',
muted: savedMuteState,
width: Ti.UI.FILL,
height: 300
});
// Save mute state when it changes
player.addEventListener('muteChanged', function(e) {
Ti.App.Properties.setBool('my_youtube_muted', e.muted);
Ti.API.info('Mute state saved: ' + e.muted);
});
win.add(player);YouTube decides quality based on connection. You can suggest a quality:
const player = YouTubePlayer.createPlayerView({
videoId: 'VIDEO_ID',
preferredQuality: 'hd1080' // or 'highres' for 4K
});Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request

