ios - hacer - gif para iphone
Creando un gran GIF con CGImageDestinationFinalize-quedándose sin memoria (2)
Puedes usar AVFoundation para escribir un video con tus imágenes. He cargado un proyecto de prueba de trabajo completo en este repositorio de github . Cuando ejecute el proyecto de prueba en el simulador, imprimirá una ruta de archivo a la consola de depuración. Abre esa ruta en tu reproductor de video para verificar la salida.
Repasaré las partes importantes del código en esta respuesta.
Comience creando un AVAssetWriter
. Le daría el tipo de archivo AVFileTypeAppleM4V
para que el video funcione en dispositivos iOS.
AVAssetWriter *writer = [AVAssetWriter assetWriterWithURL:self.url fileType:AVFileTypeAppleM4V error:&error];
Configure un diccionario de configuración de salida con los parámetros de video:
- (NSDictionary *)videoOutputSettings {
return @{
AVVideoCodecKey: AVVideoCodecH264,
AVVideoWidthKey: @((size_t)size.width),
AVVideoHeightKey: @((size_t)size.height),
AVVideoCompressionPropertiesKey: @{
AVVideoProfileLevelKey: AVVideoProfileLevelH264Baseline31,
AVVideoAverageBitRateKey: @(1200000) }};
}
Puede ajustar la velocidad de bits para controlar el tamaño de su archivo de video. He elegido el perfil de códec de forma bastante conservadora aquí ( es compatible con algunos dispositivos bastante antiguos ). Es posible que desee elegir un perfil posterior.
Luego cree un AVAssetWriterInput
con el tipo de medio AVMediaTypeVideo
y su configuración de salida.
NSDictionary *outputSettings = [self videoOutputSettings];
AVAssetWriterInput *input = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:outputSettings];
Configurar un diccionario de atributos de buffer de píxeles:
- (NSDictionary *)pixelBufferAttributes {
return @{
fromCF kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
fromCF kCVPixelBufferCGBitmapContextCompatibilityKey: @YES };
}
No tienes que especificar aquí las dimensiones del buffer de píxeles; AVFoundation los obtendrá de la configuración de salida de la entrada. Los atributos que he usado aquí son (creo) óptimos para dibujar con Core Graphics.
A continuación, cree un AVAssetWriterInputPixelBufferAdaptor
para su entrada utilizando la configuración del búfer de píxeles.
AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor
assetWriterInputPixelBufferAdaptorWithAssetWriterInput:input
sourcePixelBufferAttributes:[self pixelBufferAttributes]];
Agregue la entrada al escritor y dígale que se ponga en marcha:
[writer addInput:input];
[writer startWriting];
[writer startSessionAtSourceTime:kCMTimeZero];
A continuación le diremos a la entrada cómo obtener marcos de video. Sí, podemos hacer esto después de haberle dicho al escritor que comience a escribir:
[input requestMediaDataWhenReadyOnQueue:adaptorQueue usingBlock:^{
Este bloque va a hacer todo lo que necesitamos hacer con AVFoundation. La entrada lo llama cada vez que está listo para aceptar más datos. Es posible que pueda aceptar varios marcos en una sola llamada, por lo que realizaremos un bucle siempre que esté listo:
while (input.readyForMoreMediaData && self.frameGenerator.hasNextFrame) {
Estoy usando self.frameGenerator
para dibujar los marcos. Voy a mostrar ese código más tarde. El frameGenerator
decide cuándo frameGenerator
el video (devolviendo NO desde hasNextFrame
). También sabe cuándo debe aparecer cada cuadro en la pantalla:
CMTime time = self.frameGenerator.nextFramePresentationTime;
Para dibujar realmente el marco, necesitamos obtener un búfer de píxeles del adaptador:
CVPixelBufferRef buffer = 0;
CVPixelBufferPoolRef pool = adaptor.pixelBufferPool;
CVReturn code = CVPixelBufferPoolCreatePixelBuffer(0, pool, &buffer);
if (code != kCVReturnSuccess) {
errorBlock([self errorWithFormat:@"could not create pixel buffer; CoreVideo error code %ld", (long)code]);
[input markAsFinished];
[writer cancelWriting];
return;
} else {
Si no pudiéramos obtener un buffer de píxeles, señalamos un error y abortamos todo. Si conseguimos un búfer de píxeles, necesitamos envolver un contexto de mapa de bits a su alrededor y pedirle a frameGenerator
que dibuje el siguiente marco en el contexto:
CVPixelBufferLockBaseAddress(buffer, 0); {
CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB(); {
CGContextRef gc = CGBitmapContextCreate(CVPixelBufferGetBaseAddress(buffer), CVPixelBufferGetWidth(buffer), CVPixelBufferGetHeight(buffer), 8, CVPixelBufferGetBytesPerRow(buffer), rgb, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst); {
[self.frameGenerator drawNextFrameInContext:gc];
} CGContextRelease(gc);
} CGColorSpaceRelease(rgb);
Ahora podemos añadir el búfer al vídeo. El adaptador hace eso:
[adaptor appendPixelBuffer:buffer withPresentationTime:time];
} CVPixelBufferUnlockBaseAddress(buffer, 0);
} CVPixelBufferRelease(buffer);
}
El bucle anterior empuja los marcos a través del adaptador hasta que la entrada dice que ha tenido suficiente, o hasta que frameGenerator
dice que está fuera de los marcos. Si el frameGenerator
tiene más marcos, simplemente regresamos, y la entrada nos llamará nuevamente cuando esté listo para más marcos:
if (self.frameGenerator.hasNextFrame) {
return;
}
Si el frameGenerator
está fuera de cuadros, cerramos la entrada:
[input markAsFinished];
Y luego le decimos al escritor que termine. Llamará a un controlador de finalización cuando se hace:
[writer finishWritingWithCompletionHandler:^{
if (writer.status == AVAssetWriterStatusFailed) {
errorBlock(writer.error);
} else {
dispatch_async(dispatch_get_main_queue(), doneBlock);
}
}];
}];
En comparación, generar los cuadros es bastante sencillo. Aquí está el protocolo que el generador adopta:
@protocol DqdFrameGenerator <NSObject>
@required
// You should return the same size every time I ask for it.
@property (nonatomic, readonly) CGSize frameSize;
// I''ll ask for frames in a loop. On each pass through the loop, I''ll start by asking if you have any more frames:
@property (nonatomic, readonly) BOOL hasNextFrame;
// If you say NO, I''ll stop asking and end the video.
// If you say YES, I''ll ask for the presentation time of the next frame:
@property (nonatomic, readonly) CMTime nextFramePresentationTime;
// Then I''ll ask you to draw the next frame into a bitmap graphics context:
- (void)drawNextFrameInContext:(CGContextRef)gc;
// Then I''ll go back to the top of the loop.
@end
Para mi prueba, dibujo una imagen de fondo y la cubro lentamente con rojo sólido a medida que avanza el video.
@implementation TestFrameGenerator {
UIImage *baseImage;
CMTime nextTime;
}
- (instancetype)init {
if (self = [super init]) {
baseImage = [UIImage imageNamed:@"baseImage.jpg"];
_totalFramesCount = 100;
nextTime = CMTimeMake(0, 30);
}
return self;
}
- (CGSize)frameSize {
return baseImage.size;
}
- (BOOL)hasNextFrame {
return self.framesEmittedCount < self.totalFramesCount;
}
- (CMTime)nextFramePresentationTime {
return nextTime;
}
Core Graphics coloca el origen en la esquina inferior izquierda del contexto de mapa de bits, pero estoy usando un UIImage
, y a UIKit le gusta tener el origen en la parte superior izquierda.
- (void)drawNextFrameInContext:(CGContextRef)gc {
CGContextTranslateCTM(gc, 0, baseImage.size.height);
CGContextScaleCTM(gc, 1, -1);
UIGraphicsPushContext(gc); {
[baseImage drawAtPoint:CGPointZero];
[[UIColor redColor] setFill];
UIRectFill(CGRectMake(0, 0, baseImage.size.width, baseImage.size.height * self.framesEmittedCount / self.totalFramesCount));
} UIGraphicsPopContext();
++_framesEmittedCount;
Llamo a una devolución de llamada que utiliza mi programa de prueba para actualizar un indicador de progreso:
if (self.frameGeneratedCallback != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
self.frameGeneratedCallback();
});
}
Finalmente, para demostrar la velocidad de cuadro variable, emito la primera mitad de los cuadros a 30 cuadros por segundo, y la segunda mitad a 15 cuadros por segundo:
if (self.framesEmittedCount < self.totalFramesCount / 2) {
nextTime.value += 1;
} else {
nextTime.value += 2;
}
}
@end
Estoy tratando de solucionar un problema de rendimiento al crear GIF con muchos marcos. Por ejemplo, algunos GIF podrían contener> 1200 marcos. Con mi código actual me quedo sin memoria. Estoy tratando de averiguar cómo resolver esto; ¿Podría hacerse esto en lotes? Mi primera idea fue si era posible adjuntar imágenes juntas, pero no creo que haya un método para eso o cómo GIF crea el marco de ImageIO
. Sería bueno si existiera un método plural de CGImageDestinationAddImages
pero no lo hay, así que estoy perdido en cómo tratar de resolver esto. Aprecio cualquier ayuda ofrecida. Lo siento de antemano por el largo código, pero sentí que era necesario mostrar la creación paso a paso del GIF.
Es aceptable que pueda hacer un archivo de video en lugar de un GIF siempre que los diferentes retardos de fotograma de GIF sean posibles en un video y la grabación no tome tanto tiempo como la suma de todas las animaciones en cada fotograma.
Nota: salta al encabezado de Actualización más reciente a continuación para omitir la historia de fondo.
Actualizaciones 1 - 6: bloqueo de subproceso solucionado utilizando GCD
, pero el problema de la memoria aún permanece. La utilización del 100% de la CPU no es la preocupación aquí, ya que muestro un UIActivityIndicatorView
mientras se realiza el trabajo. El uso del método drawViewHierarchyInRect
puede ser más eficiente / rápido que el método renderInContext
, sin embargo, descubrí que no se puede usar el método drawViewHierarchyInRect
en un hilo de fondo con la propiedad afterScreenUpdates
establecida en YES; bloquea el hilo.
Debe haber alguna forma de escribir el GIF en lotes. Creo que he reducido el problema de la memoria a: CGImageDestinationFinalize
Este método parece bastante ineficiente para hacer imágenes con muchos marcos ya que todo tiene que estar en la memoria para escribir la imagen completa. He confirmado esto porque utilizo poca memoria al capturar las imágenes de la capa de CGImageDestinationAddImage
contenedor y al llamar a CGImageDestinationAddImage
. En el momento en que llamo a CGImageDestinationFinalize
el medidor de memoria aumenta instantáneamente; A veces hasta 2GB en función de la cantidad de cuadros. La cantidad de memoria requerida parece una locura para hacer un GIF de ~ 20-1000KB.
Actualización 2: hay un método que encontré que podría prometer alguna esperanza. Es:
CGImageDestinationCopyImageSource(CGImageDestinationRef idst,
CGImageSourceRef isrc, CFDictionaryRef options,CFErrorRef* err)
Mi nueva idea es que por cada 10 o algún otro número arbitrario de fotogramas, los escribiré en un destino, y luego, en el siguiente bucle, el destino completado anteriormente con 10 fotogramas ahora será mi nueva fuente. Sin embargo hay un problema; Leyendo los documentos dice esto:
Losslessly copies the contents of the image source, ''isrc'', to the * destination, ''idst''.
The image data will not be modified. No other images should be added to the image destination.
* CGImageDestinationFinalize() should not be called afterward -
* the result is saved to the destination when this function returns.
Esto me hace pensar que mi idea no funcionará, pero desgraciadamente lo intenté. Continuar con la actualización 3.
Actualización 3: He estado probando el método CGImageDestinationCopyImageSource
con mi código actualizado a continuación, sin embargo, siempre estoy recuperando una imagen con un solo cuadro; esto se debe a la documentación indicada en la Actualización 2 más probable. Todavía hay un método más para intentarlo: CGImageSourceCreateIncremental
Pero dudo que sea lo que necesito.
Parece que necesito alguna forma de escribir / anexar los marcos GIF al disco de manera incremental para poder eliminar cada fragmento nuevo de la memoria. ¿Quizás sería ideal un CGImageDestinationCreateWithDataConsumer
con las devoluciones de llamada adecuadas para guardar los datos de forma incremental?
Actualización 4: comencé a probar el método CGImageDestinationCreateWithDataConsumer
para ver si podía administrar la escritura de los bytes a medida que NSFileHandle
utilizando un NSFileHandle
, pero nuevamente el problema es que llamar a CGImageDestinationFinalize
envía todos los bytes de una sola vez, lo mismo que antes - Me quedo sin memoria. Realmente necesito ayuda para resolver esto y ofreceré una gran recompensa.
Actualización 5: He publicado una gran recompensa. Me gustaría ver algunas soluciones brillantes sin una biblioteca o un marco de terceros para adjuntar los bytes de GIF de NSData
bruto y escribirlos de forma incremental en el disco con un NSFileHandle
, esencialmente creando el GIF manualmente. O, si crees que se puede encontrar una solución utilizando ImageIO
como lo que probé, también sería increíble. Swizzling, subclases, etc.
Actualización 6: He estado investigando cómo se hacen los GIF en el nivel más bajo, y escribí una pequeña prueba que está en la línea de lo que estoy buscando con la ayuda de la recompensa. Necesito tomar el UIImage renderizado, obtener los bytes de él, comprimirlo con LZW y agregar los bytes junto con algún otro trabajo como determinar la tabla de colores global. Fuente de información: http://giflib.sourceforge.net/whatsinagif/bits_and_bytes.html .
Última actualización:
He pasado toda la semana investigando esto desde todos los ángulos para ver qué sucede exactamente para crear GIF de calidad decente basados en limitaciones (como 256 colores máximo). Creo y supongo que lo que está haciendo ImageIO
es crear un único contexto de mapa de bits bajo el capó con todos los cuadros de imagen fusionados como uno, y está realizando una cuantización de color en este mapa de bits para generar una única tabla de color global para ser utilizada en el GIF. El uso de un editor hexadecimal en algunos archivos GIF realizados con ImageIO
confirma que tienen una tabla de colores global y nunca tienen una local, a menos que la establezca para cada marco. La cuantización del color se realiza en este enorme mapa de bits para crear una paleta de colores (nuevamente, suponiendo, pero firmemente).
Tengo esta idea extraña y loca: las imágenes de marcos de mi aplicación solo pueden diferir en un color por fotograma e incluso mejor, sé qué pequeños conjuntos de colores usa mi aplicación. El primer marco de fondo es un marco que contiene colores que no puedo controlar (contenido proporcionado por el usuario, como fotos), así que lo que estoy pensando es que tomaré una instantánea de esta vista, y luego tomaré otra vista que tenga los colores conocidos que ofrece mi aplicación Con y haga de este un único contexto de mapa de bits que pueda pasar a las rutinas de creación normales de ImaegIO
GIF. ¿Cuál es la ventaja? Bueno, esto se reduce de ~ 1200 cuadros a uno al combinar dos imágenes en una sola imagen. ImageIO
luego hará lo suyo en el mapa de bits mucho más pequeño y escribirá un solo GIF con un marco.
Ahora, ¿qué puedo hacer para construir el GIF real de 1200 marcos? Estoy pensando que puedo tomar ese GIF de un solo cuadro y extraer los bytes de la tabla de colores muy bien, porque se encuentran entre dos bloques de protocolo GIF. Todavía necesitaré construir el GIF manualmente, pero ahora no debería tener que calcular la paleta de colores. Estaré robando la paleta que ImageIO
pensaba que era mejor y usaría para mi búfer de bytes. Todavía necesito una implementación de compresor LZW con la ayuda de la recompensa, pero debería ser mucho más fácil que la cuantización del color, que puede ser muy lenta. LZW también puede ser lento, así que no estoy seguro de si vale la pena; ni idea de cómo LZW se desempeñará secuencialmente con ~ 1200 cuadros.
¿Cuáles son tus pensamientos?
@property (nonatomic, strong) NSFileHandle *outputHandle;
- (void)makeGIF
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),^
{
NSString *filePath = @"/Users/Test/Desktop/Test.gif";
[[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil];
self.outputHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
NSMutableData *openingData = [[NSMutableData alloc]init];
// GIF89a header
const uint8_t gif89aHeader [] = { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 };
[openingData appendBytes:gif89aHeader length:sizeof(gif89aHeader)];
const uint8_t screenDescriptor [] = { 0x0A, 0x00, 0x0A, 0x00, 0x91, 0x00, 0x00 };
[openingData appendBytes:screenDescriptor length:sizeof(screenDescriptor)];
// Global color table
const uint8_t globalColorTable [] = { 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00 };
[openingData appendBytes:globalColorTable length:sizeof(globalColorTable)];
// ''Netscape 2.0'' - Loop forever
const uint8_t applicationExtension [] = { 0x21, 0xFF, 0x0B, 0x4E, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2E, 0x30, 0x03, 0x01, 0x00, 0x00, 0x00 };
[openingData appendBytes:applicationExtension length:sizeof(applicationExtension)];
[self.outputHandle writeData:openingData];
for (NSUInteger i = 0; i < 1200; i++)
{
const uint8_t graphicsControl [] = { 0x21, 0xF9, 0x04, 0x04, 0x32, 0x00, 0x00, 0x00 };
NSMutableData *imageData = [[NSMutableData alloc]init];
[imageData appendBytes:graphicsControl length:sizeof(graphicsControl)];
const uint8_t imageDescriptor [] = { 0x2C, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x0A, 0x00, 0x00 };
[imageData appendBytes:imageDescriptor length:sizeof(imageDescriptor)];
const uint8_t image [] = { 0x02, 0x16, 0x8C, 0x2D, 0x99, 0x87, 0x2A, 0x1C, 0xDC, 0x33, 0xA0, 0x02, 0x75, 0xEC, 0x95, 0xFA, 0xA8, 0xDE, 0x60, 0x8C, 0x04, 0x91, 0x4C, 0x01, 0x00 };
[imageData appendBytes:image length:sizeof(image)];
[self.outputHandle writeData:imageData];
}
NSMutableData *closingData = [[NSMutableData alloc]init];
const uint8_t appSignature [] = { 0x21, 0xFE, 0x02, 0x48, 0x69, 0x00 };
[closingData appendBytes:appSignature length:sizeof(appSignature)];
const uint8_t trailer [] = { 0x3B };
[closingData appendBytes:trailer length:sizeof(trailer)];
[self.outputHandle writeData:closingData];
[self.outputHandle closeFile];
self.outputHandle = nil;
dispatch_async(dispatch_get_main_queue(),^
{
// Get back to main thread and do something with the GIF
});
});
}
- (UIImage *)getImage
{
// Read question''s ''Update 1'' to see why I''m not using the
// drawViewHierarchyInRect method
UIGraphicsBeginImageContextWithOptions(self.containerView.bounds.size, NO, 1.0);
[self.containerView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *snapShot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// Shaves exported gif size considerably
NSData *data = UIImageJPEGRepresentation(snapShot, 1.0);
return [UIImage imageWithData:data];
}
Si configura kCGImagePropertyGIFHasGlobalColorMap
en NO
entonces no habrá memoria.