ConvertAndSave.m 43.8 KB
Newer Older
1
/*****************************************************************************
2
 * ConvertAndSave.m: MacOS X interface module
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
 *****************************************************************************
 * Copyright (C) 2012 Felix Paul Kühne
 * $Id$
 *
 * Authors: Felix Paul Kühne <fkuehne -at- videolan -dot- org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

#import "ConvertAndSave.h"
25
#import "intf.h"
26
#import "VLCPlaylist.h"
27
#import "misc.h"
28
#import "SharedDialogs.h"
29

30 31
#import <vlc_common.h>
#import <vlc_url.h>
32

33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
/* mini doc:
 * the used NSMatrix includes a bunch of cells referenced most easily by tags. There you go: */
#define MPEGTS 0
#define WEBM 1
#define OGG 2
#define MP4 3
#define MPEGPS 4
#define MJPEG 5
#define WAV 6
#define FLV 7
#define MPEG1 8
#define MKV 9
#define RAW 10
#define AVI 11
#define ASF 12
/* 13-15 are present, but not set */

50 51
@interface VLCConvertAndSave()
{
52 53 54
    NSArray *_videoCodecs;
    NSArray *_audioCodecs;
    NSArray *_subsCodecs;
55 56 57
    BOOL b_streaming;
}

58 59 60 61 62 63 64 65
- (void)updateDropView;
- (void)updateOKButton;
- (void)resetCustomizationSheetBasedOnProfile:(NSString *)profileString;
- (void)selectCellByEncapsulationFormat:(NSString *)format;
- (NSString *)currentEncapsulationFormatAsFileExtension:(BOOL)b_extension;
- (NSString *)composedOptions;
- (void)updateCurrentProfile;
- (void)storeProfilesOnDisk;
66
- (void)recreateProfilePopup;
67 68
@end

69
@implementation VLCConvertAndSave
70

71 72 73
#pragma mark -
#pragma mark Initialization

74 75
+ (void)initialize
{
76 77
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

Jean-Baptiste Kempf's avatar
Jean-Baptiste Kempf committed
78
    /* We are using the same format as the Qt intf here:
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
     * Container(string), transcode video(bool), transcode audio(bool),
     * use subtitles(bool), video codec(string), video bitrate(integer),
     * scale(float), fps(float), width(integer, height(integer),
     * audio codec(string), audio bitrate(integer), channels(integer),
     * samplerate(integer), subtitle codec(string), subtitle overlay(bool) */
    NSArray * defaultProfiles = [[NSArray alloc] initWithObjects:
                                 @"mp4;1;1;0;h264;0;0;0;0;0;mpga;128;2;44100;0;1",
                                 @"webm;1;1;0;VP80;2000;0;0;0;0;vorb;128;2;44100;0;1",
                                 @"ts;1;1;0;h264;800;1;0;0;0;mpga;128;2;44100;0;0",
                                 @"ts;1;1;0;drac;800;1;0;0;0;mpga;128;2;44100;0;0",
                                 @"ogg;1;1;0;theo;800;1;0;0;0;vorb;128;2;44100;0;0",
                                 @"ogg;1;1;0;theo;800;1;0;0;0;flac;128;2;44100;0;0",
                                 @"ts;1;1;0;mp2v;800;1;0;0;0;mpga;128;2;44100;0;0",
                                 @"asf;1;1;0;WMV2;800;1;0;0;0;wma2;128;2;44100;0;0",
                                 @"asf;1;1;0;DIV3;800;1;0;0;0;mp3;128;2;44100;0;0",
94 95 96 97 98
                                 @"ogg;0;1;0;none;800;1;0;0;0;vorb;128;2;44100;none;0",
                                 @"raw;0;1;0;none;800;1;0;0;0;mp3;128;2;44100;none;0",
                                 @"mp4;0;1;0;none;800;1;0;0;0;mpga;128;2;44100;none;0",
                                 @"raw;0;1;0;none;800;1;0;0;0;flac;128;2;44100;none;0",
                                 @"wav;0;1;0;none;800;1;0;0;0;s16l;128;2;44100;none;0", nil];
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121

    NSArray * defaultProfileNames = [[NSArray alloc] initWithObjects:
                                     @"Video - H.264 + MP3 (MP4)",
                                     @"Video - VP80 + Vorbis (Webm)",
                                     @"Video - H.264 + MP3 (TS)",
                                     @"Video - Dirac + MP3 (TS)",
                                     @"Video - Theora + Vorbis (OGG)",
                                     @"Video - Theora + Flac (OGG)",
                                     @"Video - MPEG-2 + MPGA (TS)",
                                     @"Video - WMV + WMA (ASF)",
                                     @"Video - DIV3 + MP3 (ASF)",
                                     @"Audio - Vorbis (OGG)",
                                     @"Audio - MP3",
                                     @"Audio - MP3 (MP4)",
                                     @"Audio - FLAC",
                                     @"Audio - CD",
                                     nil];

    NSDictionary *appDefaults = [NSDictionary dictionaryWithObjectsAndKeys:defaultProfiles, @"CASProfiles", defaultProfileNames, @"CASProfileNames", nil];

    [defaults registerDefaults:appDefaults];
}

122
- (id)init
123
{
124
    self = [super initWithWindowNibName:@"ConvertAndSave"];
125 126 127 128
    if (self) {
        self.popupPanel = [[VLCPopupPanelController alloc] init];
        self.textfieldPanel = [[VLCTextfieldPanelController alloc] init];
    }
129
    return self;
130 131
}

132
- (void)windowDidLoad
133
{
134
    [self.window setTitle: _NS("Convert & Stream")];
135 136 137 138 139 140 141 142 143 144
    [_okButton setTitle: _NS("Go!")];
    [_dropLabel setStringValue: _NS("Drop media here")];
    [_dropButton setTitle: _NS("Open media...")];
    [_profileLabel setStringValue: _NS("Choose Profile")];
    [_customizeButton setTitle: _NS("Customize...")];
    [_destinationLabel setStringValue: _NS("Choose Destination")];
    [_fileDestinationFileNameStub setStringValue: _NS("Choose an output location")];
    [_fileDestinationFileName setHidden: YES];
    [_fileDestinationBrowseButton setTitle:_NS("Browse...")];
    [_streamDestinationButton setTitle:_NS("Setup Streaming...")];
David Fuhrmann's avatar
David Fuhrmann committed
145
    [_streamDestinationURLLabel setStringValue:_NS("Select Streaming Method")];
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
    [_destinationFileButton setTitle:_NS("Save as File")];
    [_destinationStreamButton setTitle:_NS("Stream")];
    [_destinationCancelBtn setHidden:YES];

    [_customizeOkButton setTitle: _NS("Apply")];
    [_customizeCancelButton setTitle: _NS("Cancel")];
    [_customizeNewProfileButton setTitle: _NS("Save as new Profile...")];
    [[_customizeTabView tabViewItemAtIndex:0] setLabel: _NS("Encapsulation")];
    [[_customizeTabView tabViewItemAtIndex:1] setLabel: _NS("Video codec")];
    [[_customizeTabView tabViewItemAtIndex:2] setLabel: _NS("Audio codec")];
    [[_customizeTabView tabViewItemAtIndex:3] setLabel: _NS("Subtitles")];
    [_customizeTabView selectTabViewItemAtIndex: 0];
    [_customizeVidCheckbox setTitle: _NS("Video")];
    [_customizeVidKeepCheckbox setTitle: _NS("Keep original video track")];
    [_customizeVidCodecLabel setStringValue: _NS("Codec")];
    [_customizeVidBitrateLabel setStringValue: _NS("Bitrate")];
    [_customizeVidFramerateLabel setStringValue: _NS("Frame rate")];
    [_customizeVidResolutionBox setTitle: _NS("Resolution")];
    [_customizeVidResLabel setStringValue: _NS("You just need to fill one of the three following parameters, VLC will autodetect the other using the original aspect ratio")];
    [_customizeVidWidthLabel setStringValue: _NS("Width")];
    [_customizeVidHeightLabel setStringValue: _NS("Height")];
    [_customizeVidScaleLabel setStringValue: _NS("Scale")];

    [_customizeAudCheckbox setTitle: _NS("Audio")];
    [_customizeAudKeepCheckbox setTitle: _NS("Keep original audio track")];
    [_customizeAudCodecLabel setStringValue: _NS("Codec")];
    [_customizeAudBitrateLabel setStringValue: _NS("Bitrate")];
    [_customizeAudChannelsLabel setStringValue: _NS("Channels")];
    [_customizeAudSamplerateLabel setStringValue: _NS("Samplerate")];

    [_customizeSubsCheckbox setTitle: _NS("Subtitles")];
    [_customizeSubsOverlayCheckbox setTitle: _NS("Overlay subtitles on the video")];

    [_streamOkButton setTitle: _NS("Apply")];
    [_streamCancelButton setTitle: _NS("Cancel")];
    [_streamDestinationLabel setStringValue:_NS("Stream Destination")];
    [_streamAnnouncementLabel setStringValue:_NS("Stream Announcement")];
    [_streamTypeLabel setStringValue:_NS("Type")];
    [_streamAddressLabel setStringValue:_NS("Address")];
    [_streamTTLLabel setStringValue:_NS("TTL")];
    [_streamTTLStepper setEnabled:NO];
    [_streamPortLabel setStringValue:_NS("Port")];
    [_streamSAPCheckbox setStringValue:_NS("SAP Announcement")];
    [[_streamSDPMatrix cellWithTag:0] setTitle:_NS("None")];
    [[_streamSDPMatrix cellWithTag:1] setTitle:_NS("HTTP Announcement")];
    [[_streamSDPMatrix cellWithTag:2] setTitle:_NS("RTSP Announcement")];
    [[_streamSDPMatrix cellWithTag:3] setTitle:_NS("Export SDP as file")];
    [_streamSAPCheckbox setState:NSOffState];
    [_streamSDPMatrix setEnabled:NO];
195
    [_streamSDPFileBrowseButton setStringValue:_NS("Browse...")];
196 197
    [_streamChannelLabel setStringValue:_NS("Channel Name")];
    [_streamSDPLabel setStringValue:_NS("SDP URL")];
198

199
    /* there is no way to hide single cells, so replace the existing ones with empty cells.. */
200
    id blankCell = [[NSCell alloc] init];
201
    [blankCell setEnabled:NO];
202 203 204
    [_customizeEncapMatrix putCell:blankCell atRow:3 column:1];
    [_customizeEncapMatrix putCell:blankCell atRow:3 column:2];
    [_customizeEncapMatrix putCell:blankCell atRow:3 column:3];
205

206 207
    /* fetch profiles from defaults */
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
208 209 210
    [self setProfileValueList: [defaults arrayForKey:@"CASProfiles"]];
    [self setProfileNames: [defaults arrayForKey:@"CASProfileNames"]];
    [self recreateProfilePopup];
211

212
    _videoCodecs = [[NSArray alloc] initWithObjects:
213 214
                    [NSArray arrayWithObjects:@"MPEG-1", @"MPEG-2", @"MPEG-4", @"DIVX 1", @"DIVX 2", @"DIVX 3", @"H.263", @"H.264", @"VP8", @"WMV1", @"WMV2", @"M-JPEG", @"Theora", @"Dirac", nil],
                    [NSArray arrayWithObjects:@"mpgv", @"mp2v", @"mp4v", @"DIV1", @"DIV2", @"DIV3", @"H263", @"h264", @"VP80", @"WMV1", @"WMV2", @"MJPG", @"theo", @"drac", nil],
215 216
                    nil];
    _audioCodecs = [[NSArray alloc] initWithObjects:
217 218
                    [NSArray arrayWithObjects:@"MPEG Audio", @"MP3", @"MPEG 4 Audio (AAC)", @"A52/AC-3", @"Vorbis", @"Flac", @"Speex", @"WAV", @"WMA2", nil],
                    [NSArray arrayWithObjects:@"mpga", @"mp3", @"mp4a", @"a52", @"vorb", @"flac", @"spx", @"s16l", @"wma2", nil],
219 220
                    nil];
    _subsCodecs = [[NSArray alloc] initWithObjects:
221 222
                   [NSArray arrayWithObjects:@"DVB subtitle", @"T.140", nil],
                   [NSArray arrayWithObjects:@"dvbs", @"t140", nil],
223
                    nil];
224

225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
    [_customizeVidCodecPopup removeAllItems];
    [_customizeVidScalePopup removeAllItems];
    [_customizeAudCodecPopup removeAllItems];
    [_customizeAudSampleratePopup removeAllItems];
    [_customizeSubsPopup removeAllItems];

    [_customizeVidCodecPopup addItemsWithTitles:[_videoCodecs firstObject]];
    [_customizeAudCodecPopup addItemsWithTitles:[_audioCodecs firstObject]];
    [_customizeSubsPopup addItemsWithTitles:[_subsCodecs firstObject]];

    [_customizeAudSampleratePopup addItemWithTitle:@"8000"];
    [_customizeAudSampleratePopup addItemWithTitle:@"11025"];
    [_customizeAudSampleratePopup addItemWithTitle:@"22050"];
    [_customizeAudSampleratePopup addItemWithTitle:@"44100"];
    [_customizeAudSampleratePopup addItemWithTitle:@"48000"];

    [_customizeVidScalePopup addItemWithTitle:@"1"];
    [_customizeVidScalePopup addItemWithTitle:@"0.25"];
    [_customizeVidScalePopup addItemWithTitle:@"0.5"];
    [_customizeVidScalePopup addItemWithTitle:@"0.75"];
    [_customizeVidScalePopup addItemWithTitle:@"1.25"];
    [_customizeVidScalePopup addItemWithTitle:@"1.5"];
    [_customizeVidScalePopup addItemWithTitle:@"1.75"];
    [_customizeVidScalePopup addItemWithTitle:@"2"];

    [_okButton setEnabled: NO];
251

252
    // setup drop view
253 254
    [_dropBox enablePlaylistItems];
    [_dropBox setDropHandler: self];
255

256
    [self resetCustomizationSheetBasedOnProfile:[self.profileValueList firstObject]];
257 258
}

259
# pragma mark -
260
# pragma mark User Interaction - main window
261

262
- (IBAction)finalizePanel:(id)sender
263
{
264
    if (b_streaming) {
265
        if ([[[_streamTypePopup selectedItem] title] isEqualToString:@"HTTP"]) {
266
            NSString *muxformat = [self.currentProfile firstObject];
267
            if ([muxformat isEqualToString:@"wav"] || [muxformat isEqualToString:@"mov"] || [muxformat isEqualToString:@"mp4"] || [muxformat isEqualToString:@"mkv"]) {
268
                NSBeginInformationalAlertSheet(_NS("Invalid container format for HTTP streaming"), _NS("OK"), @"", @"", self.window,
269
                                               nil, nil, nil, nil,
270 271 272 273 274 275
                                               _NS("Media encapsulated as %@ cannot be streamed through the HTTP protocol for technical reasons."),
                                               [[self currentEncapsulationFormatAsFileExtension:YES] uppercaseString]);
                return;
            }
        }
    }
276

277
    playlist_t * p_playlist = pl_Get(getIntf());
278

279
    input_item_t *p_input = input_item_New([_MRL UTF8String], [[_dropinMediaLabel stringValue] UTF8String]);
280 281 282 283
    if (!p_input)
        return;

    input_item_AddOption(p_input, [[self composedOptions] UTF8String], VLC_INPUT_OPTION_TRUSTED);
284
    if (b_streaming)
285
        input_item_AddOption(p_input, [[NSString stringWithFormat:@"ttl=%@", [_streamTTLField stringValue]] UTF8String], VLC_INPUT_OPTION_TRUSTED);
286 287

    int returnValue;
288
    returnValue = playlist_AddInput(p_playlist, p_input, PLAYLIST_STOP, PLAYLIST_END, true, pl_Unlocked);
289 290 291 292

    if (returnValue == VLC_SUCCESS) {
        /* let's "play" */
        PL_LOCK;
293 294 295
        playlist_item_t *p_item = playlist_ItemGetByInput(p_playlist, p_input);
        playlist_Control(p_playlist, PLAYLIST_VIEWPLAY, pl_Locked, NULL,
                         p_item);
296 297 298
        PL_UNLOCK;
    }
    else
299
        msg_Err(getIntf(), "CAS: playlist add input failed :(");
300 301

    /* we're done with this input */
302
    vlc_gc_decref(p_input);
303

304
    [self.window performClose:sender];
305 306 307 308
}

- (IBAction)openMedia:(id)sender
{
309 310 311 312 313
    /* preliminary implementation until the open panel is cleaned up */
    NSOpenPanel * openPanel = [NSOpenPanel openPanel];
    [openPanel setCanChooseDirectories:NO];
    [openPanel setResolvesAliases:YES];
    [openPanel setAllowsMultipleSelection:NO];
314
    [openPanel beginSheetModalForWindow:self.window completionHandler:^(NSInteger returnCode) {
315 316
        if (returnCode == NSOKButton)
        {
317
            [self setMRL: toNSStr(vlc_path2uri([[[openPanel URL] path] UTF8String], NULL))];
318 319 320 321
            [self updateOKButton];
            [self updateDropView];
        }
    }];
322 323
}

324
- (IBAction)switchProfile:(id)sender
325
{
326
    NSUInteger index = [_profilePopup indexOfSelectedItem];
327 328
    // last index is "custom"
    if (index <= ([self.profileValueList count] - 1))
329
        [self resetCustomizationSheetBasedOnProfile:[self.profileValueList objectAtIndex:index]];
330
}
331

332 333
- (IBAction)deleteProfileAction:(id)sender
{
334
    /* show panel */
335 336 337 338
    [_popupPanel setTitleString:_NS("Remove a profile")];
    [_popupPanel setSubTitleString:_NS("Select the profile you would like to remove:")];
    [_popupPanel setOkButtonString:_NS("Remove")];
    [_popupPanel setCancelButtonString:_NS("Cancel")];
339 340
    [_popupPanel setPopupButtonContent:self.profileNames];

341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
    __weak typeof(self) _self = self;
    [_popupPanel runModalForWindow:self.window completionHandler:^(NSInteger returnCode, NSInteger selectedIndex) {

        if (returnCode != NSOKButton)
            return;

        /* remove requested profile from the arrays */
        NSMutableArray * workArray = [[NSMutableArray alloc] initWithArray:_self.profileNames];
        [workArray removeObjectAtIndex:selectedIndex];
        [_self setProfileNames:[[NSArray alloc] initWithArray:workArray]];
        workArray = [[NSMutableArray alloc] initWithArray:_self.profileValueList];
        [workArray removeObjectAtIndex:selectedIndex];
        [_self setProfileValueList:[[NSArray alloc] initWithArray:workArray]];

        /* update UI */
        [_self recreateProfilePopup];

        /* update internals */
        [_self switchProfile:_self];
        [_self storeProfilesOnDisk];
    }];
362 363
}

364 365
- (IBAction)iWantAFile:(id)sender
{
366 367
    NSRect boxFrame = [_destinationBox frame];
    NSRect subViewFrame = [_fileDestinationView frame];
368 369
    subViewFrame.origin.x = (boxFrame.size.width - subViewFrame.size.width) / 2;
    subViewFrame.origin.y = ((boxFrame.size.height - subViewFrame.size.height) / 2) - 15.;
370 371 372 373 374
    [_fileDestinationView setFrame: subViewFrame];
    [[_destinationFileButton animator] setHidden: YES];
    [[_destinationStreamButton animator] setHidden: YES];
    [_destinationBox performSelector:@selector(addSubview:) withObject:_fileDestinationView afterDelay:0.2];
    [[_destinationCancelBtn animator] setHidden:NO];
375
    b_streaming = NO;
376
    [_okButton setTitle:_NS("Save")];
377 378 379 380
}

- (IBAction)iWantAStream:(id)sender
{
381 382
    NSRect boxFrame = [_destinationBox frame];
    NSRect subViewFrame = [_streamDestinationView frame];
383 384
    subViewFrame.origin.x = (boxFrame.size.width - subViewFrame.size.width) / 2;
    subViewFrame.origin.y = ((boxFrame.size.height - subViewFrame.size.height) / 2) - 15.;
385 386 387 388 389
    [_streamDestinationView setFrame: subViewFrame];
    [[_destinationFileButton animator] setHidden: YES];
    [[_destinationStreamButton animator] setHidden: YES];
    [_destinationBox performSelector:@selector(addSubview:) withObject:_streamDestinationView afterDelay:0.2];
    [[_destinationCancelBtn animator] setHidden:NO];
390
    b_streaming = YES;
391
    [_okButton setTitle:_NS("Stream")];
392 393 394 395
}

- (IBAction)cancelDestination:(id)sender
{
396 397 398 399 400 401 402 403
    if ([_streamDestinationView superview] != nil)
        [_streamDestinationView removeFromSuperview];
    if ([_fileDestinationView superview] != nil)
        [_fileDestinationView removeFromSuperview];

    [_destinationCancelBtn setHidden:YES];
    [[_destinationFileButton animator] setHidden: NO];
    [[_destinationStreamButton animator] setHidden: NO];
404
    b_streaming = NO;
405 406 407
}

- (IBAction)browseFileDestination:(id)sender
408
{
409
    NSSavePanel * saveFilePanel = [NSSavePanel savePanel];
410 411
    [saveFilePanel setCanSelectHiddenExtension: YES];
    [saveFilePanel setCanCreateDirectories: YES];
412
    if ([[_customizeEncapMatrix selectedCell] tag] != RAW) // there is no clever guess for this
413
        [saveFilePanel setAllowedFileTypes:[NSArray arrayWithObject:[self currentEncapsulationFormatAsFileExtension:YES]]];
414
    [saveFilePanel beginSheetModalForWindow:self.window completionHandler:^(NSInteger returnCode) {
415 416
        if (returnCode == NSOKButton) {
            [self setOutputDestination:[[saveFilePanel URL] path]];
417 418 419
            [_fileDestinationFileName setStringValue: [[NSFileManager defaultManager] displayNameAtPath:_outputDestination]];
            [[_fileDestinationFileNameStub animator] setHidden: YES];
            [[_fileDestinationFileName animator] setHidden: NO];
420 421
        } else {
            [self setOutputDestination:@""];
422 423
            [[_fileDestinationFileName animator] setHidden: YES];
            [[_fileDestinationFileNameStub animator] setHidden: NO];
424
        }
425
        [self updateOKButton];
426
    }];
427 428
}

429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471
#pragma mark -
#pragma mark User interaction - customization panel

- (IBAction)customizeProfile:(id)sender
{
    [NSApp beginSheet:_customizePanel modalForWindow:self.window modalDelegate:self didEndSelector:NULL contextInfo:nil];
}

- (IBAction)closeCustomizationSheet:(id)sender
{
    [_customizePanel orderOut:sender];
    [NSApp endSheet: _customizePanel];

    if (sender == _customizeOkButton)
        [self updateCurrentProfile];
}



- (IBAction)videoSettingsChanged:(id)sender
{
    bool enableSettings = [_customizeVidCheckbox state] == NSOnState && [_customizeVidKeepCheckbox state] == NSOffState;
    [_customizeVidSettingsBox enableSubviews:enableSettings];
    [_customizeVidKeepCheckbox setEnabled:[_customizeVidCheckbox state] == NSOnState];
}

- (IBAction)audioSettingsChanged:(id)sender
{
    bool enableSettings = [_customizeAudCheckbox state] == NSOnState && [_customizeAudKeepCheckbox state] == NSOffState;
    [_customizeAudSettingsBox enableSubviews:enableSettings];
    [_customizeAudKeepCheckbox setEnabled:[_customizeAudCheckbox state] == NSOnState];
}

- (IBAction)subSettingsChanged:(id)sender
{
    bool enableSettings = [_customizeSubsCheckbox state] == NSOnState;
    [_customizeSubsOverlayCheckbox setEnabled:enableSettings];
    [_customizeSubsPopup setEnabled:enableSettings];
}

- (IBAction)newProfileAction:(id)sender
{
    /* show panel */
472 473 474 475
    [_textfieldPanel setTitleString: _NS("Save as new profile")];
    [_textfieldPanel setSubTitleString: _NS("Enter a name for the new profile:")];
    [_textfieldPanel setCancelButtonString: _NS("Cancel")];
    [_textfieldPanel setOkButtonString: _NS("Save")];
476

477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501
    __weak typeof(self) _self = self;
    [_textfieldPanel runModalForWindow:_customizePanel completionHandler:^(NSInteger returnCode, NSString *resultingText) {
        if (returnCode != NSOKButton || [resultingText length] == 0)
            return;

        /* prepare current data */
        [_self updateCurrentProfile];

        /* add profile to arrays */
        NSMutableArray * workArray = [[NSMutableArray alloc] initWithArray:self.profileNames];
        [workArray addObject:resultingText];
        [_self setProfileNames:[[NSArray alloc] initWithArray:workArray]];

        workArray = [[NSMutableArray alloc] initWithArray:self.profileValueList];
        [workArray addObject:[self.currentProfile componentsJoinedByString:@";"]];
        [_self setProfileValueList:[[NSArray alloc] initWithArray:workArray]];

        /* update UI */
        [_self recreateProfilePopup];
        [_profilePopup selectItemWithTitle:resultingText];

        /* update internals */
        [_self switchProfile:self];
        [_self storeProfilesOnDisk];
    }];
502 503 504 505 506
}

#pragma mark -
#pragma mark User interaction - stream panel

507 508
- (IBAction)showStreamPanel:(id)sender
{
509
    [NSApp beginSheet:_streamPanel modalForWindow:self.window modalDelegate:self didEndSelector:NULL contextInfo:nil];
510 511 512 513
}

- (IBAction)closeStreamPanel:(id)sender
{
514 515
    [_streamPanel orderOut:sender];
    [NSApp endSheet: _streamPanel];
516

517
    if (sender == _streamCancelButton)
518 519
        return;

520
    /* provide a summary of the user selections */
521
    NSMutableString * labelContent = [[NSMutableString alloc] initWithFormat:_NS("%@ stream to %@:%@"), [_streamTypePopup titleOfSelectedItem], [_streamAddressField stringValue], [_streamPortField stringValue]];
522

523 524
    if ([_streamTypePopup indexOfSelectedItem] > 1)
        [labelContent appendFormat:@" (\"%@\")", [_streamChannelField stringValue]];
525

526
    [_streamDestinationURLLabel setStringValue:labelContent];
527

528
    /* catch obvious errors */
529
    if (![[_streamAddressField stringValue] length] > 0) {
530
        NSBeginInformationalAlertSheet(_NS("No Address given"),
531
                                       _NS("OK"), @"", @"", _streamPanel, nil, nil, nil, nil,
532 533 534 535
                                       @"%@", _NS("In order to stream, a valid destination address is required."));
        return;
    }

536
    if ([_streamSAPCheckbox state] && ![[_streamChannelField stringValue] length] > 0) {
537
        NSBeginInformationalAlertSheet(_NS("No Channel Name given"),
538
                                       _NS("OK"), @"", @"", _streamPanel, nil, nil, nil, nil,
539
                                       @"%@", _NS("SAP stream announcement is enabled. However, no channel name is provided."));
540 541 542
        return;
    }

543
    if ([_streamSDPMatrix isEnabled] && [_streamSDPMatrix selectedCell] != [_streamSDPMatrix cellWithTag:0] && ![[_streamSDPField stringValue] length] > 0) {
544
        NSBeginInformationalAlertSheet(_NS("No SDP URL given"),
545
                                       _NS("OK"), @"", @"", _streamPanel, nil, nil, nil, nil,
546 547 548 549 550
                                       @"%@", _NS("A SDP export is requested, but no URL is provided."));
        return;
    }

    /* store destination for further reference and update UI */
551
    [self setOutputDestination: [_streamAddressField stringValue]];
552
    [self updateOKButton];
553 554
}

555 556
- (IBAction)streamTypeToggle:(id)sender
{
557
    NSUInteger index = [_streamTypePopup indexOfSelectedItem];
558
    if (index <= 1) { // HTTP, MMSH
559 560 561 562
        [_streamTTLField setEnabled:NO];
        [_streamTTLStepper setEnabled:NO];
        [_streamSAPCheckbox setEnabled:NO];
        [_streamSDPMatrix setEnabled:NO];
563
    } else if (index == 2) { // RTP
564 565 566 567
        [_streamTTLField setEnabled:YES];
        [_streamTTLStepper setEnabled:YES];
        [_streamSAPCheckbox setEnabled:YES];
        [_streamSDPMatrix setEnabled:YES];
568
    } else { // UDP
569 570 571 572
        [_streamTTLField setEnabled:YES];
        [_streamTTLStepper setEnabled:YES];
        [_streamSAPCheckbox setEnabled:YES];
        [_streamSDPMatrix setEnabled:NO];
573
    }
574
    [self streamAnnouncementToggle:sender];
575 576 577 578
}

- (IBAction)streamAnnouncementToggle:(id)sender
{
579 580
    [_streamChannelField setEnabled:[_streamSAPCheckbox state] && [_streamSAPCheckbox isEnabled]];
    [_streamSDPField setEnabled:[_streamSDPMatrix isEnabled] && ([_streamSDPMatrix selectedCell] != [_streamSDPMatrix cellWithTag:0])];
581

582 583
    if ([[_streamSDPMatrix selectedCell] tag] == 3)
        [_streamSDPFileBrowseButton setEnabled: YES];
584
    else
585
        [_streamSDPFileBrowseButton setEnabled: NO];
586 587 588 589 590 591 592
}

- (IBAction)sdpFileLocationSelector:(id)sender
{
    NSSavePanel * saveFilePanel = [NSSavePanel savePanel];
    [saveFilePanel setCanSelectHiddenExtension: YES];
    [saveFilePanel setCanCreateDirectories: YES];
593
    [saveFilePanel setAllowedFileTypes:[NSArray arrayWithObject:@"sdp"]];
594
    [saveFilePanel beginSheetModalForWindow:_streamPanel completionHandler:^(NSInteger returnCode) {
595
        if (returnCode == NSOKButton)
596
            [_streamSDPField setStringValue:[[saveFilePanel URL] path]];
597
    }];
598 599
}

600 601 602
#pragma mark -
#pragma mark User interaction - misc

603 604 605
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
    NSPasteboard *paste = [sender draggingPasteboard];
606
    NSArray *types = [NSArray arrayWithObjects:NSFilenamesPboardType, @"VLCPlaylistItemPboardType", nil];
607 608 609
    NSString *desired_type = [paste availableTypeFromArray: types];
    NSData *carried_data = [paste dataForType: desired_type];

610 611
    if (carried_data) {
        if ([desired_type isEqualToString:NSFilenamesPboardType]) {
612 613 614
            NSArray *values = [[paste propertyListForType: NSFilenamesPboardType] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];

            if ([values count] > 0) {
615
                [self setMRL: toNSStr(vlc_path2uri([[values firstObject] UTF8String], NULL))];
616
                [self updateOKButton];
617 618 619
                [self updateDropView];
                return YES;
            }
620
        } else if ([desired_type isEqualToString:@"VLCPlaylistItemPboardType"]) {
621 622 623
            NSArray * array = [[[VLCMain sharedInstance] playlist] draggedItems];
            NSUInteger count = [array count];
            if (count > 0) {
624
                playlist_t * p_playlist = pl_Get(getIntf());
625 626 627 628 629
                playlist_item_t * p_item = NULL;

                PL_LOCK;
                /* let's look for the first proper input item */
                for (NSUInteger x = 0; x < count; x++) {
630
                    p_item = [[array objectAtIndex:x] pointerValue];
631 632 633
                    if (p_item) {
                        if (p_item->p_input) {
                            if (p_item->p_input->psz_uri != nil) {
634
                                [self setMRL: toNSStr(p_item->p_input->psz_uri)];
635 636 637 638 639 640 641 642 643 644 645 646
                                [self updateDropView];
                                [self updateOKButton];

                                PL_UNLOCK;

                                return YES;
                            }
                        }
                    }
                }
                PL_UNLOCK;
            }
647 648 649 650 651 652 653
        }
    }
    return NO;
}

# pragma mark -
# pragma mark Private Functionality
654

655 656
- (void)updateDropView
{
657
    if ([_MRL length] > 0) {
658
        NSString * path = [[NSURL URLWithString:_MRL] path];
659
        [_dropinMediaLabel setStringValue: [[NSFileManager defaultManager] displayNameAtPath: path]];
660
        NSImage * image = [[NSWorkspace sharedWorkspace] iconForFile: path];
661
        [image setSize:NSMakeSize(128,128)];
662
        [_dropinIcon setImage: image];
663

664 665 666
        if (![_dropinView superview]) {
            NSRect boxFrame = [_dropBox frame];
            NSRect subViewFrame = [_dropinView frame];
667 668
            subViewFrame.origin.x = (boxFrame.size.width - subViewFrame.size.width) / 2;
            subViewFrame.origin.y = (boxFrame.size.height - subViewFrame.size.height) / 2;
669 670 671
            [_dropinView setFrame: subViewFrame];
            [[_dropImage animator] setHidden: YES];
            [_dropBox performSelector:@selector(addSubview:) withObject:_dropinView afterDelay:0.6];
672
        }
673
    } else {
674 675
        [_dropinView removeFromSuperview];
        [[_dropImage animator] setHidden: NO];
676 677 678
    }
}

679 680 681
- (void)updateOKButton
{
    if ([_outputDestination length] > 0 && [_MRL length] > 0)
682
        [_okButton setEnabled: YES];
683
    else
684
        [_okButton setEnabled: NO];
685 686
}

687
- (void)resetCustomizationSheetBasedOnProfile:(NSString *)profileString
688
{
689 690 691
    /* Container(string), transcode video(bool), transcode audio(bool),
    * use subtitles(bool), video codec(string), video bitrate(integer),
    * scale(float), fps(float), width(integer, height(integer),
692 693
    * audio codec(string), audio bitrate(integer), channels(integer),
    * samplerate(integer), subtitle codec(string), subtitle overlay(bool) */
694

695
    NSArray * components = [profileString componentsSeparatedByString:@";"];
696
    if ([components count] != 16) {
697
        msg_Err(getIntf(), "CAS: the requested profile '%s' is invalid", [profileString UTF8String]);
698 699
        return;
    }
700

701
    [self selectCellByEncapsulationFormat:[components firstObject]];
702 703 704
    [_customizeVidCheckbox setState:[[components objectAtIndex:1] intValue]];
    [_customizeAudCheckbox setState:[[components objectAtIndex:2] intValue]];
    [_customizeSubsCheckbox setState:[[components objectAtIndex:3] intValue]];
705
    [self setVidBitrate:[[components objectAtIndex:5] intValue]];
706
    [_customizeVidScalePopup selectItemWithTitle:[components objectAtIndex:6]];
707
    [self setVidFramerate:[[components objectAtIndex:7] intValue]];
708 709
    [_customizeVidWidthField setStringValue:[components objectAtIndex:8]];
    [_customizeVidHeightField setStringValue:[components objectAtIndex:9]];
710 711
    [self setAudBitrate:[[components objectAtIndex:11] intValue]];
    [self setAudChannels:[[components objectAtIndex:12] intValue]];
712 713
    [_customizeAudSampleratePopup selectItemWithTitle:[components objectAtIndex:13]];
    [_customizeSubsOverlayCheckbox setState:[[components objectAtIndex:15] intValue]];
714 715

    /* since there is no proper lookup mechanism in arrays, we need to implement a string specific one ourselves */
716
    NSArray * tempArray = [_videoCodecs objectAtIndex:1];
717
    NSUInteger count = [tempArray count];
718
    NSString * searchString = [components objectAtIndex:4];
719
    int videoKeep = [searchString isEqualToString:@"copy"];
720
    [_customizeVidKeepCheckbox setState:videoKeep];
721
    if ([searchString isEqualToString:@"none"] || [searchString isEqualToString:@"0"] || videoKeep) {
722
        [_customizeVidCodecPopup selectItemAtIndex:-1];
723 724
    } else {
        for (NSUInteger x = 0; x < count; x++) {
725
            if ([[tempArray objectAtIndex:x] isEqualToString: searchString]) {
726
                [_customizeVidCodecPopup selectItemAtIndex:x];
727
                break;
728 729 730
            }
        }
    }
731

732
    tempArray = [_audioCodecs objectAtIndex:1];
733
    count = [tempArray count];
734
    searchString = [components objectAtIndex:10];
735
    int audioKeep = [searchString isEqualToString:@"copy"];
736
    [_customizeAudKeepCheckbox setState:audioKeep];
737
    if ([searchString isEqualToString:@"none"] || [searchString isEqualToString:@"0"] || audioKeep) {
738
        [_customizeAudCodecPopup selectItemAtIndex:-1];
739 740
    } else {
        for (NSUInteger x = 0; x < count; x++) {
741
            if ([[tempArray objectAtIndex:x] isEqualToString: searchString]) {
742
                [_customizeAudCodecPopup selectItemAtIndex:x];
743 744 745 746 747
                break;
            }
        }
    }

748
    tempArray = [_subsCodecs objectAtIndex:1];
749
    count = [tempArray count];
750
    searchString = [components objectAtIndex:14];
751
    if ([searchString isEqualToString:@"none"] || [searchString isEqualToString:@"0"]) {
752
        [_customizeSubsPopup selectItemAtIndex:-1];
753 754
    } else {
        for (NSUInteger x = 0; x < count; x++) {
755
            if ([[tempArray objectAtIndex:x] isEqualToString: searchString]) {
756
                [_customizeSubsPopup selectItemAtIndex:x];
757 758 759 760
                break;
            }
        }
    }
761

762 763 764 765
    [self videoSettingsChanged:nil];
    [self audioSettingsChanged:nil];
    [self subSettingsChanged:nil];

766
    [self setCurrentProfile: [[NSMutableArray alloc] initWithArray:[profileString componentsSeparatedByString:@";"]]];
767 768 769 770 771
}

- (void)selectCellByEncapsulationFormat:(NSString *)format
{
    if ([format isEqualToString:@"ts"])
772
        [_customizeEncapMatrix selectCellWithTag:MPEGTS];
773
    else if ([format isEqualToString:@"webm"])
774
        [_customizeEncapMatrix selectCellWithTag:WEBM];
775
    else if ([format isEqualToString:@"ogg"])
776
        [_customizeEncapMatrix selectCellWithTag:OGG];
777
    else if ([format isEqualToString:@"ogm"])
778
        [_customizeEncapMatrix selectCellWithTag:OGG];
779
    else if ([format isEqualToString:@"mp4"])
780
        [_customizeEncapMatrix selectCellWithTag:MP4];
781
    else if ([format isEqualToString:@"mov"])
782
        [_customizeEncapMatrix selectCellWithTag:MP4];
783
    else if ([format isEqualToString:@"ps"])
784
        [_customizeEncapMatrix selectCellWithTag:MPEGPS];
785
    else if ([format isEqualToString:@"mpjpeg"])
786
        [_customizeEncapMatrix selectCellWithTag:MJPEG];
787
    else if ([format isEqualToString:@"wav"])
788
        [_customizeEncapMatrix selectCellWithTag:WAV];
789
    else if ([format isEqualToString:@"flv"])
790
        [_customizeEncapMatrix selectCellWithTag:FLV];
791
    else if ([format isEqualToString:@"mpeg1"])
792
        [_customizeEncapMatrix selectCellWithTag:MPEG1];
793
    else if ([format isEqualToString:@"mkv"])
794
        [_customizeEncapMatrix selectCellWithTag:MKV];
795
    else if ([format isEqualToString:@"raw"])
796
        [_customizeEncapMatrix selectCellWithTag:RAW];
797
    else if ([format isEqualToString:@"avi"])
798
        [_customizeEncapMatrix selectCellWithTag:AVI];
799
    else if ([format isEqualToString:@"asf"])
800
        [_customizeEncapMatrix selectCellWithTag:ASF];
801
    else if ([format isEqualToString:@"wmv"])
802
        [_customizeEncapMatrix selectCellWithTag:ASF];
803
    else
804
        msg_Err(getIntf(), "CAS: unknown encap format requested for customization");
805 806
}

807
- (NSString *)currentEncapsulationFormatAsFileExtension:(BOOL)b_extension
808
{
809
    NSUInteger cellTag = (NSUInteger) [[_customizeEncapMatrix selectedCell] tag];
810 811 812 813 814 815 816 817 818 819 820 821
    NSString * returnValue;
    switch (cellTag) {
        case MPEGTS:
            returnValue = @"ts";
            break;
        case WEBM:
            returnValue = @"webm";
            break;
        case OGG:
            returnValue = @"ogg";
            break;
        case MP4:
822 823 824 825 826
        {
            if (b_extension)
                returnValue = @"m4v";
            else
                returnValue = @"mp4";
827
            break;
828
        }
829
        case MPEGPS:
830 831 832 833 834
        {
            if (b_extension)
                returnValue = @"mpg";
            else
                returnValue = @"ps";
835
            break;
836
        }
837 838 839 840 841 842 843 844 845 846
        case MJPEG:
            returnValue = @"mjpeg";
            break;
        case WAV:
            returnValue = @"wav";
            break;
        case FLV:
            returnValue = @"flv";
            break;
        case MPEG1:
847 848 849 850 851
        {
            if (b_extension)
                returnValue = @"mpg";
            else
                returnValue = @"mpeg1";
852
            break;
853
        }
854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874
        case MKV:
            returnValue = @"mkv";
            break;
        case RAW:
            returnValue = @"raw";
            break;
        case AVI:
            returnValue = @"avi";
            break;
        case ASF:
            returnValue = @"asf";
            break;

        default:
            returnValue = @"none";
            break;
    }

    return returnValue;
}

875 876 877
- (NSString *)composedOptions
{
    NSMutableString *composedOptions = [[NSMutableString alloc] initWithString:@":sout=#transcode{"];
878
    BOOL haveVideo = YES;
879
    if ([[self.currentProfile objectAtIndex:1] intValue]) {
880
        // video is enabled
881
        if (![[self.currentProfile objectAtIndex:4] isEqualToString:@"copy"]) {
882 883 884 885 886 887 888 889 890 891 892
        [composedOptions appendFormat:@"vcodec=%@", [self.currentProfile objectAtIndex:4]];
            if ([[self.currentProfile objectAtIndex:5] intValue] > 0) // bitrate
                [composedOptions appendFormat:@",vb=%@", [self.currentProfile objectAtIndex:5]];
            if ([[self.currentProfile objectAtIndex:6] floatValue] > 0.) // scale
                [composedOptions appendFormat:@",scale=%@", [self.currentProfile objectAtIndex:6]];
            if ([[self.currentProfile objectAtIndex:7] floatValue] > 0.) // fps
                [composedOptions appendFormat:@",fps=%@", [self.currentProfile objectAtIndex:7]];
            if ([[self.currentProfile objectAtIndex:8] intValue] > 0) // width
                [composedOptions appendFormat:@",width=%@", [self.currentProfile objectAtIndex:8]];
            if ([[self.currentProfile objectAtIndex:9] intValue] > 0) // height
                [composedOptions appendFormat:@",height=%@", [self.currentProfile objectAtIndex:9]];
893 894
        } else {
            haveVideo = NO;
895
        }
896 897
    } else {
        [composedOptions appendString:@"vcodec=none"];
898
    }
899

900
    BOOL haveAudio = YES;
901
    if ([[self.currentProfile objectAtIndex:2] intValue]) {
902
        // audio is enabled
903 904 905 906
        if (![[self.currentProfile objectAtIndex:10] isEqualToString:@"copy"]) {
            if(haveVideo)
                [composedOptions appendString:@","];
            [composedOptions appendFormat:@"acodec=%@", [self.currentProfile objectAtIndex:10]];
907 908 909
            [composedOptions appendFormat:@",ab=%@", [self.currentProfile objectAtIndex:11]]; // bitrate
            [composedOptions appendFormat:@",channels=%@", [self.currentProfile objectAtIndex:12]]; // channel number
            [composedOptions appendFormat:@",samplerate=%@", [self.currentProfile objectAtIndex:13]]; // sample rate
910 911
        } else {
            haveAudio = NO;
912
        }
913
    } else {
914 915 916
        if(haveVideo)
            [composedOptions appendString:@","];

917
        [composedOptions appendString:@"acodec=none"];
918
    }
919
    if ([self.currentProfile objectAtIndex:3]) {
920 921
        if(haveVideo || haveAudio)
            [composedOptions appendString:@","];
922
        // subtitles enabled
923
        [composedOptions appendFormat:@"scodec=%@", [self.currentProfile objectAtIndex:14]];
924
        if ([[self.currentProfile objectAtIndex:15] intValue])
925 926 927
            [composedOptions appendFormat:@",soverlay"];
    }

928 929 930
    if (!b_streaming) {
        /* file transcoding */
        // add muxer
931
        [composedOptions appendFormat:@"}:standard{mux=%@", [self.currentProfile firstObject]];
932

933

934
        // add output destination
935
        [composedOptions appendFormat:@",access=file{no-overwrite},dst=%@}", _outputDestination];
936 937
    } else {
        /* streaming */
938 939 940 941 942 943
        if ([[[_streamTypePopup selectedItem] title] isEqualToString:@"RTP"])
            [composedOptions appendFormat:@":rtp{mux=ts,dst=%@,port=%@", _outputDestination, [_streamPortField stringValue]];
        else if ([[[_streamTypePopup selectedItem] title] isEqualToString:@"UDP"])
            [composedOptions appendFormat:@":standard{mux=ts,dst=%@,port=%@,access=udp", _outputDestination, [_streamPortField stringValue]];
        else if ([[[_streamTypePopup selectedItem] title] isEqualToString:@"MMSH"])
            [composedOptions appendFormat:@":standard{mux=asfh,dst=%@,port=%@,access=mmsh", _outputDestination, [_streamPortField stringValue]];